Error Handling Patterns in TypeScript: Result Types and Error Boundaries

Exceptions thrown anywhere in your application can crash an entire page or terminate a serverless function. Structured error handling — using Result types for recoverable errors and Error Boundaries for UI failures — gives you control over how your application responds when things go wrong.
The Result Type Pattern
Instead of throwing exceptions for expected failure modes, return a Result type that encodes both success and failure in the return type. The caller must handle both cases — the compiler enforces it.
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.user.findUnique({ where: { id } })
if (!user) {
return { success: false, error: new NotFoundError('User', id) }
}
return { success: true, data: user }
} catch (err) {
return { success: false, error: new DatabaseError('Failed to fetch user', err) }
}
}
Consumers must check the result before using the data:
const result = await getUser('123')
if (result.success) {
// TypeScript knows result.data is User
console.log(result.data.name)
} else {
// TypeScript knows result.error is NotFoundError | DatabaseError
handleError(result.error)
}
The pattern eliminates unhandled rejections, missing catch blocks, and assumptions that a function will always succeed. Every error path is visible at the call site.
Domain-Specific Error Classes
Generic Error instances carry no semantic meaning. Create error classes that encode what went wrong and metadata for debugging.
export class NotFoundError extends Error {
constructor(
public resource: string,
public id: string
) {
super(`${resource} with id ${id} not found`)
this.name = 'NotFoundError'
}
toResponse() {
return {
status: 404,
body: { error: this.message, resource: this.resource, id: this.id },
}
}
}
export class ValidationError extends Error {
constructor(
public field: string,
public message: string,
public value?: unknown
) {
super(`Validation failed on ${field}: ${message}`)
this.name = 'ValidationError'
}
}
export class RateLimitError extends Error {
constructor(
public retryAfter: number
) {
super(`Rate limited. Retry after ${retryAfter}s`)
this.name = 'RateLimitError'
}
}
Each error class serializes itself for API responses, keeping error handling consistent across your application.
Composing Functions with Result Types
When you chain multiple operations that can fail, Result types let you compose them without nested try-catch blocks:
async function createOrder(input: CreateOrderInput): Promise<Result<Order, AppError>> {
// Validate input
const validated = validateOrderInput(input)
if (!validated.success) return validated
// Check inventory
const inventory = await checkInventory(validated.data.productId)
if (!inventory.success) return inventory
// Process payment
const payment = await processPayment(validated.data)
if (!payment.success) return payment
// Create order
return createOrderInDb(validated.data, payment.data)
}
Each step returns a Result. If any step fails, the function returns early. The caller gets a single Result to check — no exceptions, no intermediate catch blocks.
React Error Boundaries for UI Resilience
Error Boundaries catch rendering errors in React component trees and display a fallback UI instead of crashing the entire page.
'use client'
import { Component, type ReactNode, type ErrorInfo } from 'react'
interface ErrorBoundaryProps {
children: ReactNode
fallback?: ReactNode
onError?: (error: Error, errorInfo: ErrorInfo) => void
}
interface ErrorBoundaryState {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo)
this.props.onError?.(error, errorInfo)
// Report to monitoring service
reportError(error, errorInfo)
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div className="p-6 text-center">
<h2>Something went wrong</h2>
<p>{this.state.error?.message ?? 'An unexpected error occurred'}</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
className="mt-4 px-4 py-2 bg-brand text-white rounded"
>
Try again
</button>
</div>
)
}
return this.props.children
}
}
Wrap each major section of your application in its own Error Boundary — navigation in one, main content in another, widgets in their own. One widget crashing should not take down the sidebar or the header.
export default function DashboardPage() {
return (
<div className="grid grid-cols-3 gap-4">
<ErrorBoundary fallback={<WidgetError name="Revenue" />}>
<RevenueChart />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError name="Orders" />}>
<RecentOrders />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError name="Traffic" />}>
<TrafficSources />
</ErrorBoundary>
</div>
)
}
Solid error handling transforms failures from application crashes into controlled responses. At SoniNow, we build TypeScript applications with comprehensive error strategies — from Result types in the data layer to Error Boundaries in the UI — that keep your application running smoothly even when things go wrong.
Want to harden your application's error handling? Contact SoniNow for a code review and error strategy consultation.
Related Insights

Building a Design System with React, TypeScript, and Storybook
A complete guide to building a scalable design system using React, TypeScript, and Storybook including component architecture, theming, accessibility, and documentation.

Drizzle ORM vs Prisma: Type-Safe Database Access for TypeScript Projects
A comparison of Drizzle ORM vs Prisma for TypeScript database access including query syntax, migration systems, performance, and developer experience.

GraphQL API Design: Schema-First Development with TypeScript
Learn schema-first GraphQL API development with TypeScript including type generation, resolver patterns, error handling, pagination, and subscription setup.