From Throwing Errors to Result-Based Error Handling
Why I moved from try-catch chaos to typed Result types and how it changed my approach to error recovery.
2026-06-10·8 min·intermediate
#typescript#error-handling#patterns
613 words · 197 lines
Xonoxc
Problem
For years I wrote code like this:
code
async function getUser(id: string) {
const user = await db.users.findUnique({ where: { id } })
if (!user) {
throw new NotFoundError("User not found")
}
return user
}
Then every caller needed try-catch:
code
try {
const user = await getUser("123")
renderUser(user)
} catch (error) {
if (error instanceof NotFoundError) {
renderNotFound()
} else if (error instanceof DatabaseError) {
renderError()
}
}
This works until you forget a catch. Or the error type changes. Or you need to handle errors at multiple levels.
Initial Assumptions
Implementation
The Result type forces errors into the type system:
code
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function getUser(
id: string
): Promise<Result<User, NotFoundError | DatabaseError>> {
try {
const user = await db.users.findUnique({ where: { id } })
if (!user) {
return {
success: false,
error: new NotFoundError(),
}
}
return {
success: true,
data: user,
}
} catch (error) {
return {
success: false,
error: new DatabaseError(error),
}
}
}
// Caller must handle both cases:
const result = await getUser("123")
if (!result.success) {
switch (result.error.constructor) {
case NotFoundError:
renderNotFound();
break
case DatabaseError:
renderError()
break
}
return
}
renderUser(result.data);
The compiler enforces that every possible path is handled.
Problems Encountered
Verbosity
The biggest complaint is boilerplate. Every call site needs a check.
Third-party Integration
Most libraries throw. Wrapping every call in try-catch is tedious.
Tradeoffs
Tradeoffs.md
| Aspect | Exceptions | Result Types |
|---|---|---|
| Type safety | False — errors are invisible | True — errors in signatures |
| Caller discipline | Optional | Enforced |
| Error propagation | Automatic (stack unwinding) | Explicit (must return) |
| Error recovery | At any level | At any level |
| Third-party compat | High | Requires wrapping |
| Code verbosity | Low | Medium |
| Async complexity | Low | Medium |
Final Mental Model
Improvements
- Pattern matching in TypeScript (when TC39 proposal lands)
- Effectfully-typed pipelines
- Automatic error type narrowing in switch statements
