Skip to main content

The Problem with Exceptions

Traditional exception-based error handling has several fundamental problems that make programs harder to reason about and maintain.

1. Invisible Control Flow

Exceptions create hidden control flow paths:
The function signature (order: Order) => Invoice lies about what the function actually does. It claims to always return an Invoice, but it might throw any number of different errors.

2. Unreliable Error Handling

You must trust that callers remember to catch exceptions:
With exceptions, you’re always one forgotten try/catch away from a production incident.

3. No Compile-Time Guarantees

TypeScript can’t enforce error handling:
TypeScript has no way to enforce that you handle the exception. The error only surfaces at runtime.

4. Documentation in Comments

You must document errors in JSDoc comments:
But comments:
  • Can become outdated
  • Are not enforced by the compiler
  • Are easily ignored
  • Don’t integrate with IDE autocomplete

The Solution: Encode Errors in Types

NeverThrow solves these problems by making errors part of the type system:
Now the function signature tells the complete truth about what can happen.

Benefits of Type-Encoded Errors

1. Explicit Error Handling

Errors become visible and must be handled:

2. Composition and Error Tracking

TypeScript automatically tracks all possible errors:
The compiler becomes your assistant, ensuring you never miss an error case.

3. Self-Documenting Code

The function signature is the documentation:
Your IDE shows you exactly what can go wrong:

4. Safe Refactoring

When you change error types, TypeScript tells you everywhere that needs updating:

Comparison: Exceptions vs Results

Railway-Oriented Programming

The Result pattern enables “Railway-Oriented Programming” (ROP), a powerful mental model for error handling: Think of your program as a railway:
  • Success track (Ok): Operations continue on the happy path
  • Error track (Err): Once an error occurs, we stay on the error track
Once an operation returns an Err, all subsequent operations are automatically skipped until you explicitly handle the error with match, orElse, or unwrapOr.

Real-World Example

E-commerce Order Processing

Using discriminated unions for error types (like { type: 'ErrorName', ...fields }) enables exhaustive type checking and excellent IDE support.

When to Use Each Approach

Use Result When:

  • ✅ Errors are expected and recoverable
  • ✅ You want compile-time error handling guarantees
  • ✅ You’re building an API or library
  • ✅ You want to compose operations that may fail
  • ✅ You want self-documenting error types

Exceptions May Be Appropriate For:

  • ❓ Truly exceptional, unrecoverable errors (e.g., out of memory)
  • ❓ Programming errors that should crash (e.g., assertion failures)
  • ❓ Working with third-party libraries that throw extensively
  • ❓ Prototyping (but transition to Result for production)
“Exceptional” means rare and unexpected. If you can imagine it happening during normal operation (invalid input, network failure, file not found), it’s not exceptional - it’s expected, and should be a Result.

Migrating from Exceptions

Strategy 1: Wrap at Boundaries

Wrap exception-throwing code at your system boundaries:
Source: result.ts:23-35

Strategy 2: Incremental Adoption

Start with new features, gradually migrate old code:

Strategy 3: Use fromThrowable for Quick Wins

Quickly make existing code safer:

Learning Resources from Source Code

The NeverThrow source code demonstrates the philosophy:

IResult Interface (result.ts:134-310)

Defines the contract that both Ok and Err implement, ensuring consistent behavior:

Ok Implementation (result.ts:312-417)

Shows how the “success track” short-circuits error operations:

Err Implementation (result.ts:419-521)

Shows how the “error track” short-circuits success operations:

Conclusion

By encoding errors in types, NeverThrow transforms error handling from:
  • ❌ Runtime problem → ✅ Compile-time problem
  • ❌ Hidden control flow → ✅ Explicit control flow
  • ❌ Documentation in comments → ✅ Documentation in types
  • ❌ Trust-based → ✅ Compiler-enforced
  • ❌ Easy to forget → ✅ Impossible to ignore
As the README states:
Although the package is called neverthrow, please don’t take this literally. I am simply encouraging the developer to think a bit more about the ergonomics and usage of whatever software they are writing. Throwing and catching is very similar to using goto statements - in other words; it makes reasoning about your programs harder.
Source: README.md:1675-1679
The goal isn’t to eliminate all exceptions, but to use them appropriately. Results should be your default for expected, recoverable errors.

Next Steps

Result Type

Learn the fundamentals of the Result type

ResultAsync Type

Handle asynchronous operations with ResultAsync