Skip to main content

Command Palette

Search for a command to run...

API Error Handling Standards

Published
7 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

API Error Handling Standards: A Modern TypeScript Guide for Robust Applications

Metadata

SEO Title: API Error Handling Standards: TypeScript Best Practices 2026

Meta Description: Master API error handling with modern TypeScript patterns. Learn discriminated unions, Result types, and industry standards for building resilient applications in 2026.

Keywords: API error handling, TypeScript error handling, Result type pattern, discriminated unions, error handling best practices, REST API errors, HTTP status codes, type-safe errors

Tags: TypeScript, API Design, Error Handling, Software Architecture, Backend Development, REST API, Developer Tools


The Problem: Why API Error Handling Remains Broken in 2026

Despite decades of API development, error handling remains one of the most inconsistent and poorly implemented aspects of modern software systems. Walk into any development team, and you'll find a chaotic mix of approaches: some APIs throw exceptions for everything, others return null or undefined, and many combine multiple strategies inconsistently.

The consequences are severe. According to recent industry surveys, over 40% of production incidents stem from poor error handling, and developers spend an estimated 25% of their debugging time tracking down issues caused by unclear error states. The problem isn't just technical—it's organizational and architectural.

The Root Causes

Lack of Standardization: Unlike successful patterns like REST or GraphQL for data fetching, error handling lacks widely adopted standards. HTTP status codes provide a foundation, but they're insufficient for complex application logic. Should a failed validation return 400, 422, or 409? Different teams answer differently.

Type System Limitations: Traditional exception-based error handling in languages like Java or C# doesn't compose well with modern functional patterns. Exceptions are invisible in type signatures, making it impossible to know what errors a function might produce without reading documentation or source code.

The Null/Undefined Trap: JavaScript's dual null and undefined values create ambiguity. Does null mean "no data," "error," or "not yet loaded"? This ambiguity cascades through applications, forcing defensive checks everywhere.

Microservices Complexity: In distributed systems, errors propagate across service boundaries. A single user request might touch five services, each with different error handling conventions. Aggregating and presenting these errors coherently becomes a nightmare.

Client-Side Confusion: Frontend developers face the worst of all worlds. They must handle network failures, HTTP errors, application errors, and validation errors—often with minimal type safety or guidance about which errors are recoverable.

The 2026 Landscape

Modern applications demand better. Users expect graceful degradation, not cryptic error messages. Developers need type-safe, composable error handling that works across async boundaries. Operations teams require structured, machine-readable errors for monitoring and alerting.

The good news? TypeScript's evolution, combined with lessons from functional programming languages like Rust and Haskell, has given us the tools to solve this problem. The challenge is adopting these patterns consistently.


The Modern TypeScript Solution: Type-Safe Error Handling

Foundation: The Result Type Pattern

The Result type (also called Either in functional programming) represents a computation that can succeed or fail, making errors explicit in the type system:

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

// Usage example
async function fetchUser(id: string): Promise<Result<User, UserError>> {
  try {
    const response = await fetch(`/api/users/${id}`);

    if (!response.ok) {
      return {
        success: false,
        error: {
          code: 'USER_NOT_FOUND',
          message: `User ${id} not found`,
          statusCode: 404
        }
      };
    }

    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    return {
      success: false,
      error: {
        code: 'NETWORK_ERROR',
        message: 'Failed to fetch user',
        statusCode: 500,
        cause: error
      }
    };
  }
}

Discriminated Unions for Error Types

Define specific error types using discriminated unions for exhaustive type checking:

type ApiError = 
  | { code: 'VALIDATION_ERROR'; fields: Record<string, string[]>; statusCode: 400 }
  | { code: 'UNAUTHORIZED'; message: string; statusCode: 401 }
  | { code: 'NOT_FOUND'; resource: string; statusCode: 404 }
  | { code: 'RATE_LIMITED'; retryAfter: number; statusCode: 429 }
  | { code: 'SERVER_ERROR'; message: string; statusCode: 500; cause?: unknown };

function handleError(error: ApiError): string {
  switch (error.code) {
    case 'VALIDATION_ERROR':
      return `Validation failed: ${Object.keys(error.fields).join(', ')}`;
    case 'UNAUTHORIZED':
      return 'Please log in to continue';
    case 'NOT_FOUND':
      return `${error.resource} not found`;
    case 'RATE_LIMITED':
      return `Too many requests. Retry in ${error.retryAfter}s`;
    case 'SERVER_ERROR':
      return 'An unexpected error occurred';
  }
}

Standardized Error Response Format

Implement RFC 7807 (Problem Details) for consistent API responses:

interface ProblemDetails {
  type: string;        // URI identifying the error type
  title: string;       // Human-readable summary
  status: number;      // HTTP status code
  detail?: string;     // Specific explanation
  instance?: string;   // URI identifying this occurrence
  [key: string]: unknown; // Extension fields
}

class ApiErrorResponse implements ProblemDetails {
  constructor(
    public type: string,
    public title: string,
    public status: number,
    public detail?: string,
    public instance?: string,
    public extensions?: Record<string, unknown>
  ) {}

  toJSON(): ProblemDetails {
    return {
      type: this.type,
      title: this.title,
      status: this.status,
      ...(this.detail && { detail: this.detail }),
      ...(this.instance && { instance: this.instance }),
      ...this.extensions
    };
  }
}

// Express middleware example
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  const problemDetails = new ApiErrorResponse(
    'https://api.example.com/errors/internal-error',
    'Internal Server Error',
    500,
    err.message,
    req.url
  );

  res.status(500)
    .type('application/problem+json')
    .json(problemDetails.toJSON());
});

Composable Error Handling with Helper Functions

class ResultUtils {
  static map<T, U, E>(
    result: Result<T, E>,
    fn: (value: T) => U
  ): Result<U, E> {
    return result.success 
      ? { success: true, data: fn(result.data) }
      : result;
  }

  static flatMap<T, U, E>(
    result: Result<T, E>,
    fn: (value: T) => Result<U, E>
  ): Result<U, E> {
    return result.success ? fn(result.data) : result;
  }

  static async all<T, E>(
    results: Result<T, E>[]
  ): Promise<Result<T[], E>> {
    const values: T[] = [];

    for (const result of results) {
      if (!result.success) {
        return result;
      }
      values.push(result.data);
    }

    return { success: true, data: values };
  }
}

// Usage
const userResult = await fetchUser('123');
const profileResult = ResultUtils.map(userResult, user => user.profile);

Common Pitfalls and How to Avoid Them

Pitfall 1: Swallowing Errors Silently

Problem: Catching errors without proper logging or propagation.

Solution: Always log errors with context and decide explicitly whether to recover or propagate:

async function processData(id: string): Promise<Result<Data, ApiError>> {
  const result = await fetchData(id);

  if (!result.success) {
    logger.error('Failed to fetch data', { 
      id, 
      error: result.error,
      timestamp: new Date().toISOString()
    });
    return result; // Explicit propagation
  }

  return { success: true, data: result.data };
}

Pitfall 2: Inconsistent Status Codes

Problem: Using HTTP status codes inconsistently across endpoints.

Solution: Create a mapping utility and document your conventions:

const ERROR_STATUS_MAP = {
  VALIDATION_ERROR: 400,
  UNAUTHORIZED: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  CONFLICT: 409,
  RATE_LIMITED: 429,
  SERVER_ERROR: 500
} as const;

Pitfall 3: Exposing Internal Details

Problem: Leaking stack traces or internal paths to clients.

Solution: Sanitize errors at the boundary:

function sanitizeError(error: ApiError, isProduction: boolean): ApiError {
  if (isProduction && error.code === 'SERVER_ERROR') {
    return {
      code: 'SERVER_ERROR',
      message: 'An unexpected error occurred',
      statusCode: 500
    };
  }
  return error;
}

Best Practices for Production Systems

  1. Use Structured Logging: Include correlation IDs, user context, and error codes in all logs
  2. Implement Retry Logic: Use exponential backoff for transient failures
  3. Monitor Error Rates: Set up alerts for unusual error patterns
  4. Version Your Error Codes: Treat error codes as part of your API contract
  5. Document Recovery Strategies: Tell clients which errors are retryable
  6. Test Error Paths: Write tests specifically for error scenarios
  7. Use Circuit Breakers: Prevent cascading failures in distributed systems

Frequently Asked Questions

Q: Should I use exceptions or Result types in TypeScript?

A: Use Result types for expected errors (validation, not found) and exceptions for truly exceptional circumstances (out of memory, programmer errors). Result types make error handling explicit and composable, while exceptions should represent unrecoverable situations.

Q: How do I handle errors in React components with this pattern?

A: Use custom hooks that return both data and error states:

function useUser(id: string) {
  const [state, setState] = useState<Result<User, ApiError> | null>(null);

  useEffect(() => {
    fetchUser(id).then(setState);
  }, [id]);

  return state;
}

Q: What about GraphQL error handling?

A: GraphQL has its own error format, but you can map your Result types to GraphQL errors while maintaining type safety. Use the errors array for application errors and data: null for critical failures.

Q: How do I aggregate errors from multiple API calls?

A: Use ResultUtils.all() for parallel operations or accumulate errors in an array for sequential operations where you want to collect all failures rather than fail fast.

Q: Should every function return a Result type?

A: No. Use Result types at boundaries (API calls, database operations, external services). Internal pure functions can throw exceptions or use simpler error handling since they're easier to test and debug.

Q: How do I migrate existing code to this pattern?

A: Start with new endpoints and gradually wrap existing functions. Create adapter functions that convert exception-based code to Result types at the boundary.

Q: What about performance overhead?

A: Result types have negligible overhead—they're just objects. The real cost is in network I/O and business logic. Type safety prevents bugs that cost far more than any theoretical performance impact.


Conclusion

Modern API error handling requires treating errors as first-class citizens in your type system. By adopting Result types, discriminated unions, and standardized formats like RFC 7807, you create APIs that are predictable, type-safe, and maintainable. The patterns shown here aren't just theoretical—they're battle-tested approaches used by teams building production systems at scale in 2026.