function processOrder(order: Order): Invoice { const validated = validateOrder(order) // May throw ValidationError const payment = processPayment(order) // May throw PaymentError const invoice = generateInvoice(order) // May throw InvoiceError return invoice}// The function signature doesn't tell you about any of these errors!// It's like using goto statements - you can't see where control may jump to
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.
You must trust that callers remember to catch exceptions:
// Developer A writes thisfunction fetchUser(id: number): User { if (!isValidId(id)) { throw new Error('Invalid ID') } // ... database fetch that may also throw}// Developer B uses it (forgetting error handling)const user = fetchUser(123) // 💥 Uncaught exception!console.log(user.name)
With exceptions, you’re always one forgotten try/catch away from a production incident.
function divide(a: number, b: number): number { if (b === 0) { throw new Error('Division by zero') } return a / b}// TypeScript is perfectly happy with this// even though it will crash at runtimeconst result = divide(10, 0)console.log(result * 2)
TypeScript has no way to enforce that you handle the exception. The error only surfaces at runtime.
/** * Parses user input * @throws {ParseError} If input is malformed * @throws {ValidationError} If input is invalid * @throws {NetworkError} If network fails */function parseUserInput(input: string): User { // implementation}
function divide(a: number, b: number): Result<number, string> { if (b === 0) { return err('Division by zero') } return ok(a / b)}// TypeScript forces you to handle both casesconst result = divide(10, 0)// This won't compile if you forget to handle the error:const value: number = result.value // ❌ Error: value doesn't exist on Result// You must explicitly handle it:if (result.isOk()) { const value: number = result.value // ✅ Type-safe}
// With exceptions - must read docs or source codefunction fetchUser(id: number): User// With Result - errors are obvious from signaturefunction fetchUser(id: number): Result<User, 'NotFound' | 'NetworkError' | 'Unauthorized'>// ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^// Success type All possible errors
Your IDE shows you exactly what can go wrong:
fetchUser(123). // IDE autocompletes with: // - map // - mapErr // - andThen // - match // etc.
// Function signatures lie about behaviorasync function createUser(data: UserData): Promise<User> { // Hidden error paths const validated = await validateUserData(data) // may throw const exists = await checkUserExists(validated.email) // may throw if (exists) { throw new Error('User exists') // invisible in signature } return await db.insert('users', validated) // may throw}// Caller must remember to catch (no compiler help)try { const user = await createUser(userData) res.status(200).json({ user })} catch (error) { // What types of errors can occur? Who knows! // You must read the source code or docs res.status(500).json({ error: error.message })}
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
const result = parseInput(raw) // May switch to error track .andThen(validate) // Skipped if already on error track .andThen(transform) // Skipped if already on error track .andThen(save) // Skipped if already on error track .match( (success) => handleSuccess(success), (error) => handleError(error) )
Once an operation returns an Err, all subsequent operations are automatically skipped until you explicitly handle the error with match, orElse, or unwrapOr.
❓ 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.
Wrap exception-throwing code at your system boundaries:
import { Result } from 'neverthrow'// Third-party library that throwsimport { dangerousLibrary } from 'some-library'// Wrap it immediatelyfunction safeLibraryCall(input: string): Result<Output, Error> { return Result.fromThrowable( () => dangerousLibrary.parse(input), (error) => error instanceof Error ? error : new Error(String(error)) )()}// Now use Result throughout your code
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.