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:(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:3. No Compile-Time Guarantees
TypeScript can’t enforce error handling:4. Documentation in Comments
You must document errors in JSDoc 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: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:3. Self-Documenting Code
The function signature is the documentation:4. Safe Refactoring
When you change error types, TypeScript tells you everywhere that needs updating:Comparison: Exceptions vs Results
- With Exceptions
- With 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
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)
Migrating from Exceptions
Strategy 1: Wrap at Boundaries
Wrap exception-throwing code at your system boundaries: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 bothOk 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
Although the package is calledSource: README.md:1675-1679neverthrow, 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 andcatchingis very similar to usinggotostatements - in other words; it makes reasoning about your programs harder.
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