Error Handling Patterns

Learn common error handling patterns and best practices

Error Handling Patterns

This guide covers common error handling patterns and best practices when using the go-errors library.

Core Principles

  1. Early Returns: Check for errors immediately and return early
  2. Error Propagation: Pass errors up the call stack with context
  3. Type Safety: Use custom error types with meaningful context
  4. Immutability: Results are readonly tuples
  5. Consistency: Use let for declarations and reuse error variables
  6. Error Context: Preserve error context when propagating

Basic Patterns

Synchronous Error Handling

import { goSync } from 'go-errors';

function divide(a: number, b: number) {
  let [result, err] = goSync(() => {
    if (b === 0) throw new Error('Division by zero');
    return a / b;
  });

  if (err) return [null, err] as const;
  return [result, null] as const;
}

// Usage
let [value, err] = divide(10, 2);
if (err) {
  console.error('Division failed:', err.message);
  return;
}
console.log('Result:', value);

HTTP Error Handling

import { goFetch } from 'go-errors';

interface User {
  id: string;
  firstName: string;
  lastName: string;
  fullName: string;
}

interface ApiError {
  code: string;
  message: string;
  details?: unknown;
}

async function fetchUserData(userId: string) {
  let [user, err] = await goFetch<User, ApiError>(`/api/users/${userId}`, {
    responseTransformer: (data) => ({
      ...data,
      fullName: `${data.firstName} ${data.lastName}`
    }),
    errorTransformer: (error) => {
      if (error instanceof Response) {
        return {
          code: `HTTP_${error.status}`,
          message: error.statusText
        };
      }
      return {
        code: 'UNKNOWN_ERROR',
        message: error instanceof Error ? error.message : String(error)
      };
    }
  });

if (err) {
    console.error(`Failed to fetch user: ${err.code}`, err.message);
    return [null, err] as const;
  }

  return [user, null] as const;
}

Error Types

Custom Error Types

class ValidationError extends Error {
  constructor(
    message: string,
    public field: string,
    public value: unknown
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

function validateUser(user: unknown) {
  let [result, err] = goSync<unknown, ValidationError>(() => {
    if (!user || typeof user !== 'object') {
      throw new ValidationError(
        'Invalid user object',
        'user',
        user
      );
    }
    return user;
  });

  if (err) {
    console.error(
      `Validation failed: ${err.message}`,
      `Field: ${err.field}`,
      `Value: ${err.value}`
    );
    return [null, err] as const;
  }

  return [result, null] as const;
  }

Error Hierarchies

class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public details?: unknown
  ) {
    super(message);
    this.name = 'AppError';
  }
}

class DatabaseError extends AppError {
  constructor(message: string, details?: unknown) {
    super(message, 'DATABASE_ERROR', details);
    this.name = 'DatabaseError';
  }
}

class AuthError extends AppError {
  constructor(message: string) {
    super(message, 'AUTH_ERROR');
    this.name = 'AuthError';
    }
}

Error Propagation

With Context

async function getUserData(userId: string) {
  let [user, err] = await goFetch<User>(`/api/users/${userId}`);
  if (err) {
    return [null, new AppError(
      'Failed to fetch user data',
      'USER_FETCH_ERROR',
      { userId, cause: err }
    )] as const;
}

  let [validated, err] = goSync(() => validateUser(user));
if (err) {
    return [null, new AppError(
      'User validation failed',
      'USER_VALIDATION_ERROR',
      { userId, cause: err }
    )] as const;
  }

  return [validated, null] as const;
}

Error Transformation

let [data, err] = await goFetch<User, ApiError>('/api/user', {
  errorTransformer: (error) => {
    if (error instanceof Response) {
      return {
        code: `HTTP_${error.status}`,
        message: error.statusText,
        details: { status: error.status }
      };
    }
  if (error instanceof ValidationError) {
    return {
        code: 'VALIDATION_ERROR',
        message: error.message,
        details: { field: error.field, value: error.value }
    };
  }
    return {
      code: 'UNKNOWN_ERROR',
      message: String(error)
    };
  }
});

Best Practices

  1. Always use let for result declarations:
// ✅ Good: Using let
let [value, err] = goSync(() => validate(input));
if (err) return [null, err] as const;

// ❌ Bad: Using const
const [data, error] = goSync(() => validate(input));
  1. Add meaningful context to errors:
// ✅ Good: Rich error context
class ValidationError extends Error {
  constructor(
    message: string,
    public field: string,
    public value: unknown
  ) {
    super(message);
  }
}

// ❌ Bad: Just message
throw new Error('Validation failed');
  1. Use error hierarchies for better error handling:
// ✅ Good: Error hierarchy
class AppError extends Error {
  constructor(message: string, public code: string) {
    super(message);
  }
}

class ValidationError extends AppError {
  constructor(message: string) {
    super(message, 'VALIDATION_ERROR');
  }
}

// ❌ Bad: Flat error types
class ValidationError extends Error {}
class NetworkError extends Error {}
  1. Transform errors consistently:
// ✅ Good: Consistent error transformation
let [data, err] = await goFetch<User, ApiError>('/api/user', {
  errorTransformer: (error) => ({
    code: error instanceof Response ? `HTTP_${error.status}` : 'UNKNOWN',
    message: error instanceof Error ? error.message : String(error)
  })
});

// ❌ Bad: Inconsistent error handling
let [data, err] = await goFetch('/api/user');
if (err instanceof Response) {
  // Handle response error
} else if (err instanceof Error) {
  // Handle error
}

See Also