Skip to content
advanced Phase 5 · TypeScript with Node.js

Type-Safe Error Handling

Implement Result types, typed errors, and middleware error handling.

1h
0 problems
Topic Progress 0%

Custom Error Classes

Custom Error Classes

Typed Error Hierarchy

class AppError extends Error {
  constructor(
    public readonly code: string,
    message: string,
    public readonly statusCode: number = 500,
    public readonly isOperational: boolean = true
  ) {
    super(message);
    this.name = 'AppError';
    Error.captureStackTrace(this, this.constructor);
  }
}

class ValidationError extends AppError {
  constructor(
    message: string,
    public readonly fields: Record<string, string[]>
  ) {
    super('VALIDATION_ERROR', message, 400);
    this.name = 'ValidationError';
  }
}

class NotFoundError extends AppError {
  constructor(resource: string, id: string | number) {
    super('NOT_FOUND', `${resource} with id ${id} not found`, 404);
    this.name = 'NotFoundError';
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Authentication required') {
    super('UNAUTHORIZED', message, 401);
    this.name = 'UnauthorizedError';
  }
}

Result Type Pattern

Result Type Pattern

Explicit Error Handling

type Result<T, E = AppError> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function Ok<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

function Err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

// Usage
function findUser(id: number): Result<User, NotFoundError> {
  const user = users.find(u => u.id === id);
  if (!user) {
    return Err(new NotFoundError('User', id));
  }
  return Ok(user);
}

const result = findUser(1);
if (result.ok) {
  console.log(result.value.name); // safe access
} else {
  console.error(result.error.message); // safe access
}

Async Result

async function fetchUser(id: number): Promise<Result<User>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      return Err(new AppError('FETCH_ERROR', 'Failed to fetch user', response.status));
    }
    const user: User = await response.json();
    return Ok(user);
  } catch (error) {
    return Err(new AppError('NETWORK_ERROR', 'Network error', 500));
  }
}

Discriminated Error Unions

Discriminated Error Unions

Error State Management

interface LoadingState {
  status: 'loading';
}

interface SuccessState<T> {
  status: 'success';
  data: T;
}

interface ErrorState {
  status: 'error';
  error: AppError;
}

type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;

// Usage in component
function UserComponent({ userId }: { userId: number }) {
  const [state, setState] = useState<AsyncState<User>>({ status: 'loading' });

  useEffect(() => {
    fetchUser(userId).then(result => {
      if (result.ok) {
        setState({ status: 'success', data: result.value });
      } else {
        setState({ status: 'error', error: result.error });
      }
    });
  }, [userId]);

  switch (state.status) {
    case 'loading': return <Spinner />;
    case 'error': return <ErrorMessage error={state.error} />;
    case 'success': return <UserCard user={state.data} />;
  }
}

Exhaustive Error Handling

function handleAsyncState<T>(state: AsyncState<T>): string {
  switch (state.status) {
    case 'loading': return 'Loading...';
    case 'success': return `Loaded: ${state.data}`;
    case 'error': return `Error: ${state.error.message}`;
    default: {
      const _exhaustive: never = state;
      return _exhaustive;
    }
  }
}

Error Recovery Patterns

Error Recovery Patterns

Retry Logic

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  delay: number = 1000
): Promise<Result<T>> {
  let lastError: Error;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const value = await fn();
      return Ok(value);
    } catch (error) {
      lastError = error as Error;
      if (attempt < maxRetries) {
        await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1)));
      }
    }
  }

  return Err(new AppError(
    'RETRY_EXHAUSTED',
    `Failed after ${maxRetries} retries: ${lastError!.message}`,
    500
  ));
}

// Usage
const result = await withRetry(() => fetch('https://api.example.com/data'));

Error Boundary Pattern

function catchAsync<T>(
  fn: () => Promise<T>,
  errorHandler: (error: Error) => T
): Promise<T> {
  return fn().catch(error => errorHandler(error));
}

// Usage
const data = await catchAsync(
  () => fetchData(),
  (error) => {
    logger.error('Fetch failed:', error);
    return defaultData;
  }
);

Best Practices

  • Use Result type for operations that can fail predictably
  • Create specific error classes for different failure modes
  • Never swallow errors silently - log or propagate
  • Use discriminated unions for async state management
  • Make error handling explicit in function signatures