Discussions about error handling often become:
exceptions are bad
or
Result types create boilerplate
but the more useful question is:
What does this failure mean
to the caller?
Some failures are normal outcomes:
invalid email
product out of stock
username already exists
authentication rejected
Others indicate that ordinary execution cannot continue:
database connection unexpectedly lost
required configuration missing
disk operation failed
invariant violated
Those categories often deserve different handling mechanisms.
Use the mechanism that communicates intent
Result-style values are strong when a caller is expected to inspect and branch on failure. Exceptions are strong when failure needs to escape several layers before a meaningful recovery point exists. A healthy system can use both and translate between them at deliberate boundaries.
1. Start by classifying the failure
Before choosing syntax, classify the event.
Expected business outcome
reserveSeat()
possible outcomes:
Reserved
AlreadyReserved
EventFull
None of these necessarily means the system malfunctioned.
Expected invalid input
parseAge("abc")
→ invalid number
Invalid input is part of the function's normal operating domain.
Recoverable technical failure
remote service unavailable
The caller may:
retry
fall back
show temporary error
Unexpected programming failure
impossible state reached
array invariant broken
required object unexpectedly null
Pretending this is an ordinary business outcome can hide a bug.
Fatal process condition
Some environments have failures for which continuing safely is not realistic.
Those may belong to:
panic
fatal error
process termination
rather than normal Result handling.
2. Understand what exceptions are good at
Exceptions allow failure to cross intermediate functions without every function changing its return type.
Conceptual call chain
handleRequest()
↓
createInvoice()
↓
saveInvoice()
↓
databaseDriver.write()
↓
DatabaseUnavailable
If:
createInvoice()
cannot meaningfully recover from a database outage, propagating the exception may be clearer than forcing it to handle:
DatabaseUnavailable
locally.
Stack unwinding
Exceptions can unwind through multiple stack frames until a handler is found.
This is useful when intermediate functions only know:
I cannot complete my operation
and a higher layer knows:
return HTTP 503
Exceptions integrate naturally with cleanup mechanisms
Patterns such as:
try / finally
defer
using / with
RAII
help release resources as execution leaves a scope.
Exceptions preserve diagnostic context well when used correctly
They can carry:
- Error type.
- Message.
- Stack trace.
- Original cause.
- Structured metadata.
Where exceptions become problematic
Problems appear when exceptions represent ordinary branching:
try:
findUser()
catch UserNotFound:
createUser()
if:
not found
is expected and common.
They also become difficult when:
any function may throw
anything
at any time
and API contracts do not communicate likely failure modes.
3. Understand what Result types are good at
Exceptions vs Result types decision tree (diagram)
A Result-style type makes success and failure explicit in the return value.
Result<User, ParseError>
conceptually means:
Success(User)
or
Failure(ParseError)
The caller cannot pretend the failure does not exist
result = parseUser(input)
match result:
success(user)
failure(error)
Exact syntax differs by language.
Result types are useful for expected branches
Examples:
parse configuration value
validate command
check inventory
authenticate credentials
reserve resource
decode user input
Typed failures improve APIs
Prefer:
Result<Order, OrderError>
where:
OrderError =
ProductNotFound
OutOfStock
InvalidQuantity
PaymentDeclined
over:
Result<Order, String>
Why strings are weak error contracts
The caller should not need:
if error.message contains
"out of stock"
to determine behavior.
Result types can compose
Languages and libraries often support:
map
flatMap
andThen
bind
?
match
so successful values continue while failures propagate automatically.
Result types also have costs
If every low-level failure appears in every function signature:
Result<A, DatabaseError>
Result<B, FileError>
Result<C, NetworkError>
application code can become dominated by plumbing.
That is a sign to reconsider:
which failures are actually
part of the caller's contract?
4. Model expected domain failures explicitly
Suppose a transfer operation can legitimately fail because:
account missing
insufficient balance
currency unsupported
daily limit exceeded
Those conditions are part of the business model.
Explicit domain error
TransferResult =
Success(Transfer)
AccountNotFound
InsufficientFunds
UnsupportedCurrency
DailyLimitExceeded
lets the caller map each outcome intentionally.
User-facing layer
InsufficientFunds
↓
"Your available balance
is too low for this transfer."
API layer
DailyLimitExceeded
↓
HTTP 409 or domain-specific response
depending on the API design.
Do not throw because a user made a normal choice
A payment being declined may be important, but it is not necessarily a software defect.
Treating it as:
UnhandledPaymentException
can produce noisy monitoring and confusing logs.
Expected does not mean unimportant
A typed Result can still be:
measured
logged where appropriate
shown to user
used in analytics
without pretending the application crashed.
5. Let unexpected failures propagate appropriately
Imagine:
calculateInvoice()
expects:
tax configuration loaded
actual:
configuration object missing
If that state violates a program invariant, returning:
Result<Invoice, MissingConfig>
from every billing function may hide that the system itself is broken.
Unexpected failures need visibility
They should often reach:
application boundary
monitoring
crash reporting
incident logs
Do not recover blindly
Dangerous:
catch Exception:
return emptyInvoice()
because:
empty invoice
may now look valid while hiding data corruption.
Failing loudly can be safer than continuing incorrectly
Especially when dealing with:
- Financial calculations.
- Authorization.
- Data integrity.
- Critical configuration.
Distinguish programmer errors from environmental failures
Both may use exceptions in some languages, but operational policy can differ.
database timeout:
possibly recoverable
violated invariant:
likely bug
6. Separate validation from exceptional failure
Validation is often predictable.
Example:
registerUser(input)
may need to report:
email invalid
password too short
name missing
Fail-fast validation
first error
↓
return
can be appropriate when later checks depend on earlier ones.
Accumulating validation
Forms often benefit from:
[
InvalidEmail,
PasswordTooShort,
MissingName
]
so the user can fix several problems at once.
Exceptions are usually awkward for multi-error validation
Throwing:
InvalidEmailException
immediately prevents collecting:
password
+
name
+
phone
issues in the same pass.
Separate parsing from validation when useful
raw input
↓
parse
↓
typed value
↓
validate business rules
This lets callers distinguish:
cannot understand input
from:
input is validly structured
but violates a rule
7. Translate errors between architectural layers
A database layer might produce:
UniqueConstraintViolation
but the application layer may care about:
EmailAlreadyRegistered
Those are different abstractions.
Infrastructure layer
SQL unique constraint
"users_email_key"
Repository layer
DuplicateUserRecord
Application layer
EmailAlreadyRegistered
API layer
409 Conflict
The translation keeps database vocabulary out of the client contract.
Preserve original causes
When converting:
DatabaseTimeout
into:
ServiceUnavailable
retain diagnostic cause information internally.
Conceptually:
ServiceUnavailable
caused by DatabaseTimeout
Do not expose internal exceptions directly to users
Avoid responses such as:
NullPointerException
at BillingService.java:148
to an end user.
Translate into:
safe external message
+
internal diagnostic context
8. Recover only at meaningful boundaries
Error propagation across application layers (diagram)
A catch block should answer:
Why am I catching this here?
Good answers include:
- I can retry safely.
- I can provide a fallback.
- I can translate it into a domain error.
- I own the request boundary.
- I must release or rollback resources.
Weak catch block
try:
save()
catch:
log("failed")
and then continue as though:
save succeeded
Boundary ownership
A web request handler might own:
exception
↓
log once
↓
map to safe HTTP response
A UI screen may own user recovery
Result:
NetworkUnavailable
↓
show:
"You're offline"
↓
offer:
Retry
Logging every layer creates duplicates
repository logs error
service logs same error
controller logs same error
global handler logs same error
one failure becomes four alerts.
Prefer one ownership point
Lower layers can add structured context to the error.
The owning boundary can decide:
log level
response
retry
alerting
9. Treat retries, timeouts, and cancellation separately
Not every failure should trigger:
retry
Potentially retryable
temporary network failure
service unavailable
rate limit with retry guidance
transaction conflict
Usually not retryable without changing input
invalid email
permission denied
unsupported currency
malformed request
Retry classification belongs in the error model
Better than:
catch Exception:
retry 5 times
is:
if error is transient:
retry according to policy
Retries need idempotency
Retrying:
chargeCreditCard()
after an ambiguous timeout can accidentally produce:
duplicate charge
unless the operation supports:
idempotency key
or equivalent deduplication
Timeout and failure are not identical
A timeout may mean:
caller stopped waiting
while the remote operation may still be running.
Cancellation is often a control-flow signal
If a user closes a screen:
Cancelled
may not deserve:
ERROR
production incident
Do not accidentally convert cancellation into generic failure
Preserve the runtime's intended cancellation semantics.
10. Design partial success and batch failures deliberately
Suppose you process:
1,000 imported contacts
and:
997 succeed
3 fail validation
A single exception may not express the useful outcome.
Batch result
ImportResult
successful:
997
failed:
[
row 14: InvalidEmail,
row 218: MissingName,
row 907: InvalidPhone
]
Partial success can be first-class
Useful models include:
all succeeded
some succeeded
none succeeded
operation itself failed
Differentiate item failure from system failure
row 17 invalid
is different from:
database unavailable
before import began
Transactional operations may need all-or-nothing semantics
If:
partial update
would corrupt business state
use:
transaction
rollback
global failure
rather than partial success.
11. Preserve error ownership in async code
Async operations usually represent:
future success
or
future failure
Awaited exception model
try:
result = await fetchData()
catch NetworkError:
recover()
Awaited Result model
result = await fetchData()
match result:
Success(data)
NetworkUnavailable
Unauthorized
Both can be coherent.
The same decision rule still applies
Ask whether:
NetworkUnavailable
is an expected caller-visible branch or an exceptional condition that should propagate.
Detached tasks need explicit error ownership
start background task
never await it
never observe result
risks:
lost exception
silent partial failure
unobserved rejection
Structured concurrency helps
Parent scopes can own:
child completion
child errors
child cancellation
Concurrent operations need aggregation semantics
If three tasks produce:
A = success
B = failure
C = success
decide:
fail whole operation?
return partial result?
cancel siblings?
collect every error?
rather than relying accidentally on whichever task finishes first.
12. Use a consistent error-handling workflow
Error classification and recovery workflow (diagram)
Step 1: classify the failure
expected domain outcome?
invalid input?
transient infrastructure problem?
unexpected bug?
fatal condition?
Step 2: identify who can act on it
Ask:
Can the immediate caller
make a meaningful decision?
If yes:
typed Result
may be a strong choice
If no:
propagation
may be clearer
Step 3: preserve technical context
Keep:
original cause
operation
safe identifiers
dependency context
Step 4: translate at layer boundaries
SQL error
↓
repository error
↓
domain error
↓
API / UI response
Step 5: recover only when safe
Recovery options:
retry
fallback
ask user to correct input
skip failed batch item
rollback
terminate request
Step 6: log where ownership exists
Avoid:
log
rethrow
log
rethrow
log
rethrow
Step 7: test the failure path
Include:
expected failure
unexpected dependency failure
timeout
cancellation
retry exhaustion
cleanup
Step 8: improve the contract
If callers repeatedly ask:
what kind of error is this?
introduce:
typed variants
or
better exception hierarchy
13. Avoid common error-handling anti-patterns
Catch everything and continue
catch Exception:
return null
converts:
unknown failure
into:
missing value
and destroys diagnostic information.
Result with string error
Result<User, String>
forces callers to interpret prose instead of types.
Exception for every normal branch
InvalidPromoCodeException
CouponExpiredException
CouponAlreadyUsedException
can make normal business decisions look like crashes.
Result wrapping every imaginable technical error
This:
Result<User,
NetworkError |
DatabaseError |
SerializationError |
CacheError |
ConfigError>
is not automatically better if the caller can only:
return failure
Logging and rethrowing at every layer
creates duplicate incidents and noisy logs.
Destroying the original cause
Weak:
catch DatabaseError:
throw ServiceError("failed")
Better:
ServiceError
caused by DatabaseError
Using exceptions for high-frequency control flow
Besides clarity concerns, some runtimes make exception creation or stack unwinding significantly more expensive than ordinary branching.
Returning null instead of an explicit outcome
A null may ambiguously mean:
not found
error
not loaded
not applicable
Explicit:
Option
Result
NotFound variant
can communicate intent more clearly.
Retrying all errors
Retrying:
InvalidPassword
five times does not improve the password.
Ignoring cleanup during failure
Every design should consider:
locks
transactions
files
connections
temporary state
when control exits unexpectedly.
14. Copy/paste error-handling checklist
Error handling checklist
Failure classification
- Is this expected?
- Is this a normal business outcome?
- Is this invalid user input?
- Is this transient infrastructure failure?
- Is this programmer error?
- Is this invariant violation?
- Is this fatal process condition?
- Is cancellation involved?
- Is timeout involved?
Choosing Result
- Use Result when failure is expected.
- Use Result when immediate caller should branch.
- Use Result for parse failures.
- Use Result for validation.
- Use Result for business-rule rejection.
- Use Result for expected not-found where appropriate.
- Use Result for conflict outcomes.
- Use typed error variants.
- Avoid Result<T, String> for important APIs.
- Keep error variants meaningful.
Choosing exceptions
- Use exceptions where language ecosystem expects them.
- Use exceptions for failures that must cross several layers.
- Use exceptions for unexpected technical failures where appropriate.
- Use exceptions when intermediate callers cannot recover.
- Preserve stack trace.
- Preserve original cause.
- Use cleanup constructs.
- Avoid throwing for ordinary high-frequency branching.
Language idioms
- Follow language conventions.
- Do not force Result style into a language where every library throws without a plan.
- Do not force exception style into an ecosystem centered on typed Results without reason.
- Keep public APIs consistent.
- Use adapters at boundaries where conventions differ.
Expected domain errors
- Model expected outcomes explicitly.
- Use domain vocabulary.
- Keep user-facing decisions separate from infrastructure errors.
- Do not log ordinary user mistakes as production incidents.
- Make callers handle important branches.
Typed errors
- Define error enum / sealed type / hierarchy.
- Use stable machine-readable variants.
- Keep human-readable message separate.
- Add safe metadata where useful.
- Avoid parsing messages.
- Avoid one enormous global error enum.
- Keep errors close to domain boundaries.
Error metadata
- Include operation.
- Include safe resource identifier.
- Include retryability when useful.
- Include cause.
- Include dependency.
- Avoid secrets.
- Avoid passwords.
- Avoid tokens.
- Avoid excessive personal data.
Validation
- Treat user validation as expected.
- Decide fail-fast vs accumulate.
- Return multiple field errors where useful.
- Separate parsing from business validation.
- Keep field identity.
- Provide correction guidance.
- Preserve valid submitted input.
Parsing
- Use Result-like outcome where malformed input is expected.
- Distinguish missing from malformed.
- Include safe location information.
- Avoid exceptions for routine parse probing if API already offers explicit parse result.
- Test edge cases.
Not found
- Decide whether absence is normal.
- Use Option / Maybe when absence alone matters.
- Use Result when absence needs reason or metadata.
- Use exception only when absence violates expected contract.
- Keep repository and domain semantics distinct.
Null
- Do not use null for every failure.
- Distinguish absent from failed.
- Distinguish not loaded from not found.
- Use explicit types where available.
- Avoid turning unknown exceptions into null.
Infrastructure
- Preserve technical failure.
- Translate at application boundary.
- Do not expose database implementation details externally.
- Keep retry information.
- Keep original cause.
- Add dependency context safely.
Database
- Distinguish constraint conflict.
- Distinguish timeout.
- Distinguish unavailable.
- Distinguish deadlock.
- Distinguish malformed query / programmer error.
- Translate expected constraint conflicts into domain outcomes.
- Preserve unknown database errors.
Network
- Distinguish DNS.
- Distinguish connect timeout.
- Distinguish read timeout.
- Distinguish TLS.
- Distinguish authentication.
- Distinguish rate limit.
- Distinguish server failure.
- Distinguish malformed response.
- Do not retry every network failure automatically.
Files
- Distinguish missing file.
- Distinguish permission denied.
- Distinguish invalid format.
- Distinguish disk full.
- Distinguish transient lock where relevant.
- Preserve path safely.
- Avoid exposing sensitive filesystem paths to users.
Layer boundaries
- Infrastructure error should not automatically leak to domain.
- Domain error should not depend on SQL vocabulary.
- UI should receive user-actionable outcome.
- API should receive stable external error contract.
- Preserve cause internally.
- Translate once at meaningful boundary.
Repository layer
- Translate storage-specific expected failures.
- Preserve unexpected infrastructure failures.
- Avoid swallowing database exceptions.
- Keep not-found semantics clear.
- Keep duplicate-key semantics clear.
Application layer
- Express business outcomes.
- Decide recovery.
- Apply retry policy.
- Coordinate transactions.
- Translate infrastructure error where useful.
- Keep domain vocabulary.
Controller / API layer
- Map domain error to response.
- Return safe message.
- Return stable error code.
- Log unexpected failure once.
- Do not expose stack trace.
- Add correlation ID.
UI layer
- Show actionable message.
- Offer retry when appropriate.
- Preserve user input.
- Distinguish validation from server failure.
- Do not show internal exception names.
- Handle cancellation quietly where appropriate.
Exception hierarchy
- Keep hierarchy meaningful.
- Avoid hundreds of tiny exception classes without value.
- Separate recoverable categories where caller needs distinction.
- Preserve cause.
- Avoid catching base Exception unless boundary requires it.
- Re-throw unknown errors when necessary.
Catch blocks
- Catch only when you can act.
- Recover.
- Translate.
- Retry.
- Roll back.
- Add required context.
- Terminate boundary safely.
- Do not catch only to ignore.
Catch-all handlers
- Use at application boundary where necessary.
- Log unexpected error.
- Return safe failure response.
- Keep process policy explicit.
- Do not silently continue corrupted operation.
- Preserve shutdown behavior.
Logging
- Log at ownership boundary.
- Avoid log-and-rethrow at every layer.
- Add correlation context.
- Add safe operation metadata.
- Use severity based on impact.
- Do not log expected validation as server error.
- Avoid duplicate stack traces.
Error messages
- Write for intended audience.
- Internal messages can contain technical context.
- External messages should be safe and actionable.
- Do not expose secrets.
- Do not expose stack traces to users.
- Keep machine-readable error code separate from prose.
Preserving cause
- Keep original exception.
- Use cause / inner exception feature.
- Preserve stack information.
- Avoid replacing technical failure with generic message.
- Add context without destroying cause.
Recovery
- Recover only when state remains valid.
- Prefer explicit fallback.
- Avoid inventing fake success.
- Roll back partial state when required.
- Preserve data integrity.
- Report inability to continue clearly.
Fallbacks
- Use fallback only when semantically valid.
- Cache fallback should indicate possible staleness where relevant.
- Avoid returning empty collection after unknown failure if empty means valid data.
- Monitor fallback usage.
- Test fallback failure.
Retries
- Retry transient failures only.
- Limit attempts.
- Use backoff.
- Add jitter where appropriate.
- Respect overall deadline.
- Respect cancellation.
- Check idempotency.
- Avoid retry storms.
- Record final failure.
Idempotency
- Make repeated writes safe where retries possible.
- Use operation ID.
- Use idempotency key for external writes where supported.
- Handle duplicate message delivery.
- Test ambiguous timeout.
- Avoid duplicate side effects.
Timeouts
- Define operation deadline.
- Distinguish timeout from dependency failure.
- Know whether underlying operation continues.
- Cancel where possible.
- Preserve timeout context.
- Avoid one global timeout for every operation.
Cancellation
- Treat cancellation as special control flow where runtime does.
- Propagate cancellation.
- Do not log normal user cancellation as error.
- Clean up resources.
- Avoid converting cancellation to generic failure.
- Test cancellation timing.
Resource cleanup
- Close files.
- Release connections.
- Release locks.
- Roll back transactions.
- Dispose subscriptions.
- Clean temporary resources.
- Use finally / defer / RAII.
- Test cleanup after exceptions.
Transactions
- Define commit boundary.
- Roll back on failure.
- Do not catch and continue inside corrupt transaction.
- Translate expected constraint conflict.
- Preserve unknown errors.
- Keep transaction scope small.
Batch operations
- Decide all-or-nothing vs partial success.
- Return per-item failures if useful.
- Preserve item identity.
- Separate item validation from system failure.
- Report summary counts.
- Avoid one bad item crashing entire import unless required.
Partial success
- Represent explicitly.
- List successful items.
- List failed items.
- Include typed reasons.
- Avoid calling partial operation simply "Success".
- Define retry behavior.
Async code
- Await owned operations.
- Observe failures.
- Avoid lost background exceptions.
- Define cancellation.
- Define timeout.
- Use structured concurrency where available.
- Define aggregate failure semantics.
- Preserve cause across async boundaries.
Concurrent tasks
- Decide fail-fast vs collect-all.
- Decide whether sibling tasks are cancelled.
- Decide whether partial result is valid.
- Bound concurrency.
- Preserve task identity.
- Aggregate errors intentionally.
Background tasks
- Assign owner.
- Handle failure.
- Log at supervisor.
- Define restart policy.
- Define shutdown.
- Avoid anonymous fire-and-forget critical work.
Assertions
- Use for programmer assumptions where appropriate.
- Do not use assertions for ordinary user input validation.
- Avoid recovering from violated invariants as ordinary business outcome.
- Keep production behavior understood.
Fatal errors
- Define which failures require process termination.
- Avoid attempting unsafe recovery.
- Flush critical telemetry where possible.
- Restart through supervisor when architecture supports it.
- Keep fatal policy rare and explicit.
Result composition
- Use map for success transformation.
- Use flatMap / bind for dependent Results.
- Propagate failures without repetitive branching.
- Avoid deeply nested matches where combinators improve clarity.
- Keep error types manageable.
Result anti-patterns
- Do not return Result when failure cannot be handled meaningfully by caller.
- Avoid huge unions of unrelated technical errors.
- Avoid string-only errors.
- Avoid wrapping every function just for ideological consistency.
- Avoid hiding programmer bugs inside Result variants.
Exception anti-patterns
- Do not catch everything.
- Do not swallow exceptions.
- Do not use exceptions as ordinary loop control.
- Do not destroy stack traces.
- Do not expose internal exceptions to users.
- Do not log same exception at every layer.
API design
- Document expected errors.
- Use stable error codes.
- Keep HTTP status and domain code separate where useful.
- Avoid exposing database errors.
- Keep validation structured.
- Include correlation identifier for unexpected server failures.
Library design
- Follow ecosystem conventions.
- Document thrown exceptions.
- Document Result variants.
- Keep API predictable.
- Avoid mixing styles randomly inside one module.
- Provide non-throwing variant where ecosystem supports it and use case warrants it.
Testing Results
- Test success.
- Test every important error variant.
- Test mapping.
- Test propagation.
- Test recovery.
- Test serialization of public error contracts.
Testing exceptions
- Assert exception type.
- Assert relevant message / metadata.
- Assert cause.
- Assert cleanup.
- Assert rollback.
- Avoid overly broad expected-exception tests.
Testing retries
- Fail first attempt.
- Succeed later.
- Exhaust attempts.
- Verify backoff policy where practical.
- Verify cancellation stops retries.
- Verify non-retryable errors do not retry.
- Verify side effects remain safe.
Testing validation
- Test one invalid field.
- Test multiple invalid fields.
- Test valid input.
- Test boundary values.
- Test malformed input.
- Verify user-actionable messages.
Observability
- Count unexpected failures.
- Count domain rejections separately.
- Track retry rate.
- Track timeout rate.
- Track cancellation separately.
- Track fallback rate.
- Track error code distribution.
- Alert on impact, not every expected failure.
Code review
- Is failure expected?
- Should caller branch on it?
- Is Result typed?
- Is exception appropriate?
- Where is recovery boundary?
- Is original cause preserved?
- Is error logged once?
- Is retry safe?
- Is cancellation preserved?
- Is cleanup guaranteed?
- Could this return fake success?
- Does external message leak internals?
Final review
- Does each failure category have clear meaning?
- Are expected domain failures explicit?
- Are unexpected failures visible?
- Do Result types expose typed variants?
- Are exceptions allowed to propagate until a meaningful boundary?
- Are low-level errors translated into domain vocabulary?
- Is original cause preserved?
- Is logging owned by one boundary?
- Are validation errors separate from infrastructure failures?
- Are retries restricted to transient failures?
- Are repeated writes idempotent where necessary?
- Are timeouts distinct from ordinary errors?
- Is cancellation preserved?
- Are resources released on every exit path?
- Are partial-success operations represented explicitly?
- Are async failures always observed?
- Are fatal and invariant failures kept separate from routine user errors?
- Can callers tell which failures they are expected to handle?
15. FAQ
When should I use a Result type?
Use a Result-style return value when failure is a normal part of the operation and the immediate caller is expected to inspect it and choose a branch. Parsing, validation, expected conflicts, business-rule rejection, and some not-found cases are common examples.
When should I use exceptions?
Exceptions are useful when normal execution cannot continue, when the immediate caller cannot recover meaningfully, or when a technical failure needs to travel through several layers before reaching an ownership boundary.
Should every exception be caught?
No. Catch where you can recover, translate the failure, retry safely, perform required cleanup, or terminate a request or application boundary safely. Otherwise, propagating the exception is often clearer.
Should Result errors be strings?
Usually not for important APIs. Typed variants let callers branch on stable machine-readable concepts such as NotFound, Conflict, ValidationError, PermissionDenied, or Unavailable without parsing text.
Can I use exceptions internally and Results externally?
Yes. This is a common pattern. Infrastructure or libraries can use exceptions, while an application boundary catches known technical failures, preserves their causes, and translates them into Result variants expected by higher-level callers.
Is a timeout an error?
It is a distinct outcome that should usually remain identifiable. A timeout means a deadline was exceeded; it does not always prove that the underlying operation failed or stopped.
Should validation throw exceptions?
Routine user validation is normally better represented as explicit errors because invalid input is expected. Exceptions can still be appropriate if the validation subsystem itself fails unexpectedly.
Key terms (quick glossary)
- Exception
- A runtime mechanism that transfers control from a failing operation through the call stack until an appropriate handler is reached.
- Result type
- A return type that explicitly represents either a successful value or a failure value.
- Either
- A sum type commonly used to represent one of two alternatives, often an error on one side and a successful value on the other.
- Typed error
- A structured error represented by a distinct type or variant rather than only a human-readable string.
- Domain error
- A failure or rejection expressed using concepts from the application's business domain, such as insufficient funds or inventory unavailable.
- Infrastructure error
- A technical failure originating from databases, networks, files, external services, operating systems, or similar dependencies.
- Error propagation
- Passing a failure toward a higher-level caller until code capable of handling or translating it is reached.
- Recovery boundary
- A layer that owns enough context to retry, fall back, translate, report, roll back, or safely terminate an operation.
- Cause
- The underlying failure retained inside a higher-level error or exception so diagnostic information is not lost during translation.
- Fail-fast
- Stopping an operation as soon as the first relevant failure is detected.
- Error accumulation
- Collecting multiple independent failures, commonly used for forms and validation where users benefit from seeing several corrections at once.
- Idempotency
- A property that allows the same operation to be repeated without producing unintended additional side effects.
- Retryable error
- A failure likely to succeed on a later attempt without changing the logical request, subject to safe retry policy.
- Cancellation
- A control signal indicating that work is no longer required and should stop where possible.
- Partial success
- An operation outcome where some requested items or sub-operations succeed while others fail.
- Invariant
- A condition the program assumes must always hold in a valid internal state.
Worth reading
Recommended guides from the category.