Interactive demonstration of type-safe error handling in TypeScript
Pattern 1Simple Enum Error Types
The simplest error handling pattern uses string literal unions to represent different error states.
This is perfect when you just need to distinguish between different failure modes without carrying additional context.
When the authentication function runs, it checks conditions in order:
Password validation: If the password is missing or less than 8 characters, returns err('InvalidCredentials')
Account status check: If the username is 'locked_user', returns err('AccountLocked')
Success case: If all checks pass, returns ok(user) with user data
The Result type forces you to handle both success and error cases explicitly using .match() or other combinators.
Try it yourself:
Test different error scenarios:
✨ Why use simple enum errors?
TypeScript exhaustiveness checking: The compiler ensures you handle all error cases in switch statements
Minimal overhead: Just string literals, no complex objects
Clear intent: Error types are self-documenting
Perfect for binary conditions: When you don't need to pass extra data about what went wrong
Pattern 2Union Error Types with Data
When errors need context, use discriminated unions. Each error type is an object with a type field
(the discriminator) plus additional fields carrying relevant information. This lets you pass detailed error information up the call stack.
This pattern chains multiple operations that can each fail with different error types:
Step 1 - Find product: Searches for product by ID, can return NotFoundError or DatabaseError
Step 2 - Validate product: If found, validates the update data, can return ValidationError
Error propagation: If either step fails, the error (with all its context) is returned immediately
Success case: Only if both steps succeed do we get the updated product
The .match() method uses TypeScript's discriminated unions to give you type-safe access to error-specific fields.
Try it yourself:
Test different error scenarios:
✨ Why use union error types with data?
Rich error context: Pass field names, attempted values, original errors, etc.
Debugging friendly: All the information you need to diagnose issues is attached to the error
Type-safe error handling: TypeScript narrows the type based on the discriminator, giving you autocomplete for error-specific fields
Composable: Different functions can return different error types, and they compose naturally via union types
No exceptions: All error paths are explicit in the type signature
Pattern 3Coalescing Multiple Results
When processing multiple operations at once, neverthrow provides utilities to combine results. You can either
fail fast (stop at first error) or collect all errors (continue through all operations).
// Fail fast - stops at first error
Result.combine(results): Result<T[], E>
// Collect all errors - continues through all operations
Result.combineWithAllErrors(results): Result<T[], E[]>
🔍 What's happening under the hood:
Result.combine():
Processes results in order until it hits the first error
Returns immediately with that error (fail-fast behavior)
If all succeed, returns ok([...all values])
Error type is singular: Result<T[], E>
Result.combineWithAllErrors():
Processes ALL results regardless of failures
Collects every error into an array
Only returns ok if ALL operations succeeded
Error type is an array: Result<T[], E[]>
Manual accumulation:
Useful when you want partial success (some items valid, some invalid)
Gives you both the successful results AND all the errors
Perfect for validation scenarios where you want to show all issues at once
Try it yourself:
Test different combination strategies:
💡 Test with different order IDs:
123,456,789 - Try all invalid IDs (except 123) to see multiple errors
123 - Single valid ID (all succeed)
123,456 - Mix of valid and invalid
error,123,456 - Database error followed by other errors
Remember: Only ID '123' is valid. Any other ID will produce a NotFoundError. ID 'error' produces a DatabaseError.
✨ When to use each strategy:
Use Result.combine() when:
You need all operations to succeed for the result to be valid
Early termination saves resources (e.g., no point continuing if first step fails)
You only care about the first error
Use Result.combineWithAllErrors() when:
You want to show users ALL validation errors at once
Each operation is independent (e.g., validating form fields)
You need a complete picture of what went wrong
Use manual accumulation when:
Partial success is acceptable (some items can fail)
You want to process what succeeded AND report what failed
You need custom aggregation logic beyond just arrays
🎓 Summary
neverthrow provides three main patterns for type-safe error handling:
1️⃣ Simple Enum Errors
Use when errors don't need extra data. Fast, simple, TypeScript-exhaustive.
2️⃣ Union Error Types with Data
Use when errors need context. Provides rich debugging information while maintaining type safety.
3️⃣ Combining Multiple Results
Use when processing multiple operations. Choose between fail-fast and collect-all-errors based on your needs.
The key benefit: All error paths are explicit in your type signatures. No silent failures, no forgotten error handling,
no exceptions bubbling up unexpectedly. TypeScript forces you to handle every error case, making your code more reliable.