Lab 11: Advanced Error Handling

Time: 40 minutes | Level: Advanced | Docker: docker run -it --rm node:20-alpine sh

Build exhaustive, type-safe error handling with never, custom Result monads, typed error unions, and the neverthrow library.


Step 1: Environment Setup

docker run -it --rm node:20-alpine sh
npm install -g typescript ts-node
mkdir lab11 && cd lab11
npm init -y
npm install neverthrow
echo '{"compilerOptions":{"module":"commonjs","target":"es2020","strict":true,"esModuleInterop":true}}' > tsconfig.json

💡 TypeScript's never type is the "bottom type" — it represents values that can never exist. It's the superpower behind exhaustive type checking.


Step 2: assertNever for Exhaustive Checks

The never trick ensures every case is handled at compile time:

// exhaustive.ts

// assertNever: TypeScript ensures this is never called
function assertNever(x: never, message?: string): never {
  throw new Error(message ?? `Unexpected value: ${JSON.stringify(x)}`);
}

// Discriminated union — all variants must be handled
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return 0.5 * shape.base * shape.height;
    default:
      // If you add a new Shape variant without handling it here,
      // TypeScript will error: "Argument of type X is not assignable to parameter of type never"
      return assertNever(shape);
  }
}

type Color = 'red' | 'green' | 'blue';
function getHex(color: Color): string {
  switch (color) {
    case 'red':   return '#FF0000';
    case 'green': return '#00FF00';
    case 'blue':  return '#0000FF';
    default:      return assertNever(color);
  }
}

console.log('Circle area:', area({ kind: 'circle', radius: 5 }).toFixed(2));
console.log('Rectangle area:', area({ kind: 'rectangle', width: 4, height: 6 }));
console.log('Red hex:', getHex('red'));
console.log('Blue hex:', getHex('blue'));

💡 Try adding a new variant to Shape without adding a case — TypeScript immediately errors. This is "exhaustive pattern matching" — a compile-time safety net.


Step 3: Result Monad from Scratch

Build a minimal, ergonomic Result type:


Step 4: Typed Error Unions

Model all application errors in one discriminated union:


Step 5: neverthrow Library

neverthrow provides a polished Result type with a fluent API:


Step 6: Type-Safe Error Serialization

Serialize errors to JSON safely with full type information:


Step 7: Composing Error Handlers


Step 8: Capstone — Production Error System

Run:

📸 Verified Output:


Summary

Pattern
Code
Benefit

Exhaustive check

assertNever(x: never): never

Compile error on missing cases

Result monad

type Result<T,E> = Ok<T> | Err<E>

Typed errors, no throw

Error union

type AppError = | {code:'NOT_FOUND'} | ...

Exhaustive handling

neverthrow

ok/err/ResultAsync

Polished Result API

Retry

Higher-order withRetry(fn, n, pred)

Resilience pattern

Serialization

Type-safe serializeToJson(unknown)

Safe error logging

Last updated