Skip to main content

When to Use Result vs Throwing

Choosing between Result types and throwing exceptions is a fundamental design decision.

Use Result When:

Expected failures that are part of the business logic should return Result:
Result forces callers to handle errors explicitly:
Use Result when chaining operations with different error types:
Result provides compile-time guarantees about error types:

Use Throwing When:

Unrecoverable errors that indicate bugs or system failures:
Wrap throwing third-party code with Result.try or Result.tryPromise:
In tight loops, throwing can be faster than Result for the success path:

Error Type Design Patterns

Discriminated Error Unions

Use TaggedError to create discriminated unions:

Error Hierarchies

Group related errors under a common type:

Error Context Enrichment

Add context as errors propagate:

Performance Considerations

Memory Overhead

Result creates wrapper objects. In tight loops, this can add overhead:

Short-Circuit Efficiency

Result.gen short-circuits on first error, avoiding unnecessary work:

Avoid Premature Unwrapping

Keep values in Result context as long as possible:

Type Safety Tips

Never Use any with Result

Explicitly type error unions:

Use InferOk and InferErr

Extract types from Result types:

Constrain Generic Functions

Use type constraints for generic Result functions:

Testing Strategies

Test Both Paths

Always test success and error paths:

Use Type Guards in Tests

Test Error Composition

Verify error unions in composed operations:

Mock with Result

Create test fixtures returning Results:

Common Patterns

Optional to Result

Convert nullable values to Results:

Result to Promise

Convert Result to Promise for async contexts:

Collect Results

Gather multiple Results into a single Result:

Anti-Patterns to Avoid

Don’t use Result.unwrap() without checking: This defeats the purpose of Result. Always use type guards or pattern matching.
Don’t mix Result and throwing in the same function: Choose one error handling strategy per function.
Don’t ignore errors with void operators: This silences errors without handling them.

Migration Strategy

Introduce Result gradually into existing codebases:
1

Start at the boundaries

Convert external API calls and I/O operations to use Result first.
2

Wrap throwing code

Use Result.try and Result.tryPromise to wrap existing throwing functions.
3

Define error types

Create TaggedError classes for your domain errors.
4

Convert layer by layer

Gradually convert internal functions to return Result, starting from leaf functions.
5

Update call sites

Replace try-catch with Result combinators and pattern matching.
You can have throwing code and Result-based code coexist during migration. Use Result.try at the boundaries to convert between the two styles.