본문으로 건너뛰기
Previous
Next
TypeScript Error Handling Patterns | Result Types· never

TypeScript Error Handling Patterns | Result Types· never

TypeScript Error Handling Patterns | Result Types· never

이 글의 핵심

TypeScript's type system can make error handling explicit and type-safe ??no more silent catch blocks. This guide covers Result types, discriminated unions, and patterns that make errors visible and handleable.

Why TypeScript Error Handling Needs Improvement

The default JavaScript/TypeScript error handling has a critical problem:

// What is `error`? unknown. What can you do with it?
try {
  const data = await fetchUser(id)
  return data
} catch (error) {
  console.error(error)  // What type is this?
  return null           // silent failure ??caller doesn't know
}

Problems:

  1. catch (error) gives you unknown ??no type info
  2. Functions don’t declare what errors they can throw
  3. Errors are invisible in the type signature ??callers don’t know to handle them
  4. Forgetting a try/catch compiles fine

1. The Result Type Pattern

Model errors as values, not exceptions:

// Define Result type
type Ok<T> = { success: true; data: T }
type Err<E> = { success: false; error: E }
type Result<T, E = Error> = Ok<T> | Err<E>

// Helper constructors
const ok = <T>(data: T): Ok<T> => ({ success: true, data })
const err = <E>(error: E): Err<E> => ({ success: false, error })

// Function that returns Result instead of throwing
async function fetchUser(id: string): Promise<Result<User, 'NOT_FOUND' | 'NETWORK_ERROR'>> {
  try {
    const response = await fetch(`/api/users/${id}`)
    if (response.status === 404) return err('NOT_FOUND')
    if (!response.ok) return err('NETWORK_ERROR')
    return ok(await response.json())
  } catch {
    return err('NETWORK_ERROR')
  }
}

// Caller must handle both cases ??TypeScript enforces it
const result = await fetchUser('123')

if (result.success) {
  console.log(result.data.name)  // TypeScript knows this is User
} else {
  // TypeScript knows error is 'NOT_FOUND' | 'NETWORK_ERROR'
  switch (result.error) {
    case 'NOT_FOUND':
      return showNotFoundPage()
    case 'NETWORK_ERROR':
      return showRetryButton()
  }
}

2. neverthrow Library

neverthrow provides a production-ready Result type with chaining:

npm install neverthrow
import { ok, err, Result, ResultAsync } from 'neverthrow'

// Synchronous Result
function divide(a: number, b: number): Result<number, 'DIVISION_BY_ZERO'> {
  if (b === 0) return err('DIVISION_BY_ZERO')
  return ok(a / b)
}

// Async Result
function fetchUser(id: string): ResultAsync<User, ApiError> {
  return ResultAsync.fromPromise(
    fetch(`/api/users/${id}`).then(r => r.json()),
    (e) => ({ code: 'FETCH_ERROR', message: String(e) } as ApiError)
  )
}

// Chain Results (like Promise.then)
const result = await fetchUser('123')
  .map(user => ({ ...user, displayName: `${user.firstName} ${user.lastName}` }))
  .mapErr(err => ({ ...err, timestamp: Date.now() }))
  .match(
    (user) => `Hello, ${user.displayName}`,   // success branch
    (err) => `Error: ${err.message}`           // error branch
  )

// Combine multiple Results
import { combine, combineWithAllErrors } from 'neverthrow'

const results = combine([
  fetchUser('alice'),
  fetchUser('bob'),
  fetchUser('carol'),
])

if (results.isOk()) {
  const [alice, bob, carol] = results.value
}

3. Discriminated Unions for Error Types

Define specific, typed error types:

// Typed error union
type AppError =
  | { type: 'VALIDATION_ERROR'; field: string; message: string }
  | { type: 'NOT_FOUND'; resource: string; id: string }
  | { type: 'UNAUTHORIZED'; reason: string }
  | { type: 'RATE_LIMITED'; retryAfter: number }
  | { type: 'INTERNAL_ERROR'; error: Error }

// Handler with exhaustive type checking
function handleError(error: AppError): Response {
  switch (error.type) {
    case 'VALIDATION_ERROR':
      return Response.json({ field: error.field, message: error.message }, { status: 400 })
    case 'NOT_FOUND':
      return Response.json({ message: `${error.resource} not found` }, { status: 404 })
    case 'UNAUTHORIZED':
      return Response.json({ reason: error.reason }, { status: 401 })
    case 'RATE_LIMITED':
      return new Response(null, {
        status: 429,
        headers: { 'Retry-After': String(error.retryAfter) }
      })
    case 'INTERNAL_ERROR':
      console.error(error.error)
      return Response.json({ message: 'Internal server error' }, { status: 500 })
    default:
      // TypeScript ensures this is unreachable
      const _exhaustive: never = error
      return Response.json({ message: 'Unknown error' }, { status: 500 })
  }
}

The never assignment at the bottom is the exhaustiveness check ??if you add a new error type without handling it, TypeScript shows an error.


4. Safe Parsing Pattern

Validate external data (API responses, user input) without trusting types:

import { z } from 'zod'

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'viewer']),
})

type User = z.infer<typeof UserSchema>

// Safe parse returns Result-like object
async function getUser(id: string): Promise<Result<User, AppError>> {
  const response = await fetch(`/api/users/${id}`)
  const raw = await response.json()

  const parsed = UserSchema.safeParse(raw)
  if (!parsed.success) {
    return err({
      type: 'VALIDATION_ERROR',
      field: parsed.error.issues[0]?.path.join('.') ?? 'unknown',
      message: parsed.error.message,
    })
  }

  return ok(parsed.data)
}

5. Error Boundaries in React

import { Component, ReactNode } from 'react'

interface Props { children: ReactNode; fallback: ReactNode }
interface State { hasError: boolean; error: Error | null }

class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false, error: null }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, info: { componentStack: string }) {
    // Log to error tracking service
    reportError(error, info)
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback
    }
    return this.props.children
  }
}

// Usage
<ErrorBoundary fallback={<ErrorPage message="Something went wrong" />}>
  <UserDashboard />
</ErrorBoundary>

6. Global Error Handler (Express / Hono)

// Express
import { Request, Response, NextFunction } from 'express'

interface AppError extends Error {
  statusCode?: number
  code?: string
}

app.use((err: AppError, req: Request, res: Response, next: NextFunction) => {
  const status = err.statusCode ?? 500
  const code = err.code ?? 'INTERNAL_ERROR'

  if (status >= 500) {
    console.error({ code, message: err.message, stack: err.stack })
  }

  res.status(status).json({
    error: { code, message: status < 500 ? err.message : 'Internal server error' },
    requestId: req.headers['x-request-id'],
  })
})

// Custom error class
class HttpError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string,
  ) {
    super(message)
    this.name = 'HttpError'
  }
}

// Usage in route handlers
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await db.users.findById(req.params.id)
    if (!user) throw new HttpError(404, 'NOT_FOUND', 'User not found')
    res.json(user)
  } catch (err) {
    next(err)  // passes to global error handler
  }
})

7. Async Error Handling with Promise.allSettled

// When you need all results, even if some fail
const results = await Promise.allSettled([
  fetchUser('alice'),
  fetchUser('bob'),
  fetchUser('carol'),
])

const users: User[] = []
const errors: string[] = []

for (const result of results) {
  if (result.status === 'fulfilled') {
    users.push(result.value)
  } else {
    errors.push(result.reason.message)
  }
}

// vs Promise.all which fails fast on first error
try {
  const [alice, bob, carol] = await Promise.all([
    fetchUser('alice'),
    fetchUser('bob'),
    fetchUser('carol'),
  ])
} catch (err) {
  // only know about the first failure
}

8. Error Tracking in Production

// Sentry integration
import * as Sentry from '@sentry/node'

Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 0.1 })

// Capture with context
function handleCriticalError(error: Error, context: Record<string, unknown>) {
  Sentry.withScope(scope => {
    scope.setExtras(context)
    scope.setLevel('error')
    Sentry.captureException(error)
  })
}

// Custom error classes get better Sentry grouping
class DatabaseError extends Error {
  constructor(
    message: string,
    public query: string,
    public params: unknown[]
  ) {
    super(message)
    this.name = 'DatabaseError'
  }
}

Pattern Selection Guide

ScenarioPattern
Library/utility functionsResult type (neverthrow)
HTTP route handlersthrow HttpError + global handler
React componentsError Boundary
External API callsResult type + Zod safe parse
Multiple parallel requestsPromise.allSettled
Type-safe switch dispatchDiscriminated union + never exhaustive check
Production monitoringSentry + custom error classes

Key Takeaways

  1. Make errors visible ??use Result types for functions that can fail in expected ways
  2. Type your errors ??discriminated unions let TypeScript enforce exhaustive handling
  3. Validate at boundaries ??parse external data with Zod, never trust types at runtime
  4. One global handler ??catch unhandled errors in one place, never silently swallow
  5. Use never for exhaustiveness ??TypeScript will catch missing cases at compile time

The most important shift: stop thinking of errors as exceptional ??they’re just another return value. When errors are part of the type signature, the compiler helps you handle them. Silent failures become compile errors.


?�주 묻는 질문 (FAQ)

Q. ???�용???�무?�서 ?�제 ?�나??

A. Master TypeScript error handling beyond try/catch. Covers Result types, discriminated unions, the neverthrow library, er???�무?�서????본문???�제?� ?�택 가?�드�?참고???�용?�면 ?�니??

Q. ?�행?�로 ?�으�?좋�? 글?�?

A. �?글 ?�단???�전 글 ?�는 관??글 링크�??�라가�??�서?��?배울 ???�습?�다. C++ ?�리�?목차?�서 ?�체 ?�름???�인?????�습?�다.

Q. ??깊이 공�??�려�?

A. cppreference?� ?�당 ?�이브러�?공식 문서�?참고?�세?? 글 말�???참고 ?�료 링크???�용?�면 좋습?�다.


같이 보면 좋�? 글 (?��? 링크)

??주제?� ?�결?�는 ?�른 글?�니??

  • [TypeScript 5 Complete Guide | Decorators· satisfies](/en/blog/typescript-5-complete-guide/
  • [FastAPI Complete Guide | Python REST API· Async](/en/blog/fastapi-complete-guide/

??글?�서 ?�루???�워??(관??검?�어)

TypeScript, Error Handling, JavaScript, Backend, Best Practices ?�으�?검?�하?�면 ??글???��????�니??