Error Handling Patterns in TypeScript: Result Types and Error Boundaries | SoniNow Blog

Limited TimeLearn More

typescripterror handlingresult typesreact error boundary

Error Handling Patterns in TypeScript: Result Types and Error Boundaries

Published

2026-06-23

Read Time

4 mins

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.