Create Custom Error Types in TypeScript

Recently, I was working on a large TypeScript project where we needed better error handling than just using the default Error class. We have to handle different types of errors, like validation issues, API failures, or business logic problems, in an organized way.

The issue with the standard error class is that it doesn’t give you enough context about what went wrong in the code.

In TypeScript, by using a custom error type, we can add more context to errors, like error codes or extra details, and make error handling easier to manage.

In this TypeScript tutorial, I’ll explain how to create custom error types in TypeScript, enabling efficient analysis of errors in the code.

Why Use Custom Error Types in TypeScript?

Before we jump into implementation, let’s talk about why custom error types are valuable:

  1. Better error classification
  2. Type-safe error handling
  3. Additional contextual information
  4. Improved debugging experience

Create Custom Error Types in TypeScript

We will discuss multiple ways to create custom error types in TypeScript. These include simple class extensions, adding custom properties, using factory patterns, building error hierarchies, and attaching error codes for more structured handling.

Method 1: Extending the Error Class in TypeScript

The simplest way to create a custom error type is by extending the built-in Error class:

class DatabaseError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'DatabaseError';

    // This is necessary for proper stack trace in TypeScript
    Object.setPrototypeOf(this, DatabaseError.prototype);
  }
}

// Usage
try {
  throw new DatabaseError('Failed to connect to database');
} catch (error) {
  if (error instanceof DatabaseError) {
    console.error('Database issue:', error.message);
  } else {
    console.error('Unknown error:', error);
  }
}

Output:

Error type in TypeScript

The Object.setPrototypeOf() line is crucial because TypeScript’s transpiled output can mess with the prototype chain. Without it, the instanceof checks might not work correctly.

Check out: TypeScript Record vs Object

Method 2: Adding Custom Properties

For more complex scenarios, you might want to add additional properties to your error:

class ApiError extends Error {
  statusCode: number;
  endpoint: string;

  constructor(message: string, statusCode: number, endpoint: string) {
    super(message);
    this.name = 'ApiError';
    this.statusCode = statusCode;
    this.endpoint = endpoint;

    // Fix the prototype chain
    Object.setPrototypeOf(this, ApiError.prototype);
  }

  isServerError(): boolean {
    return this.statusCode >= 500;
  }
}

// Usage
try {
  throw new ApiError('User profile not found', 404, '/api/users/123');
} catch (error) {
  if (error instanceof ApiError) {
    console.error(
      `API Error (${error.statusCode}) at ${error.endpoint}: ${error.message}`
    );

    if (error.isServerError()) {
      console.error('This is a server-side issue.');
    }
  }
}

Output:

TypeScript Custom Error Types

This approach gives you the ability to include HTTP status codes, API endpoints, or any other contextual information that helps diagnose the issue.

Check out: Conditionally Add Property to Object in TypeScript

Method 3: Using Error Factories

Sometimes it’s helpful to have factories that create standardized error instances:

// Error types
type ValidationErrorType = 'required' | 'format' | 'range';

// Base class
class ValidationError extends Error {
  field: string;
  errorType: ValidationErrorType;

  constructor(message: string, field: string, errorType: ValidationErrorType) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.errorType = errorType;

    Object.setPrototypeOf(this, ValidationError.prototype);
  }
}

// Factory functions
const ValidationErrorFactory = {
  required: (field: string) => 
    new ValidationError(`The field '${field}' is required`, field, 'required'),

  format: (field: string, format: string) => 
    new ValidationError(`The field '${field}' must be in ${format} format`, field, 'format'),

  range: (field: string, min: number, max: number) => 
    new ValidationError(`The field '${field}' must be between ${min} and ${max}`, field, 'range')
};

// Usage
function validateUserForm(data: any) {
  if (!data.email) {
    throw ValidationErrorFactory.required('email');
  }

  if (!data.email.includes('@')) {
    throw ValidationErrorFactory.format('email', 'valid email');
  }

  if (data.age && (data.age < 18 || data.age > 120)) {
    throw ValidationErrorFactory.range('age', 18, 120);
  }
}

This factory approach gives you consistent error messages and makes validation logic more readable.

Check out: Check Types in TypeScript

Method 4: Error Hierarchies for Complex Applications

For larger applications, you might want to create a hierarchy of error types:

// Base application error
class AppError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AppError';
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

// Domain-specific errors
class UserError extends AppError {
  userId?: string;

  constructor(message: string, userId?: string) {
    super(message);
    this.name = 'UserError';
    this.userId = userId;
    Object.setPrototypeOf(this, UserError.prototype);
  }
}

class PaymentError extends AppError {
  transactionId?: string;

  constructor(message: string, transactionId?: string) {
    super(message);
    this.name = 'PaymentError';
    this.transactionId = transactionId;
    Object.setPrototypeOf(this, PaymentError.prototype);
  }
}

// Even more specific errors
class PaymentDeclinedError extends PaymentError {
  declineCode: string;

  constructor(message: string, transactionId: string, declineCode: string) {
    super(message, transactionId);
    this.name = 'PaymentDeclinedError';
    this.declineCode = declineCode;
    Object.setPrototypeOf(this, PaymentDeclinedError.prototype);
  }
}

With this hierarchy, you can catch errors at different levels of specificity:

try {
  // Some code that might throw various errors
} catch (error) {
  if (error instanceof PaymentDeclinedError) {
    // Handle specific payment decline
    console.error(`Payment declined (code: ${error.declineCode}) for transaction ${error.transactionId}`);
  } else if (error instanceof PaymentError) {
    // Handle any payment issue
    console.error(`Payment error for transaction ${error.transactionId || 'unknown'}`);
  } else if (error instanceof AppError) {
    // Handle any application error
    console.error('Application error:', error.message);
  } else {
    // Handle system or third-party errors
    console.error('Unexpected error:', error);
  }
}

Output:

Create Custom Error Type in TypeScript

Check out: TypeScript Enums vs Types

Method 5: Using Error Codes

For internationalization or integration with other systems, adding error codes can be valuable:

interface ErrorDetails {
  code: string;
  details?: Record<string, any>;
}

class CodedError extends Error implements ErrorDetails {
  code: string;
  details?: Record<string, any>;

  constructor(message: string, code: string, details?: Record<string, any>) {
    super(message);
    this.name = 'CodedError';
    this.code = code;
    this.details = details;

    Object.setPrototypeOf(this, CodedError.prototype);
  }
}

// Usage example
throw new CodedError(
  'Your subscription has expired',
  'SUBSCRIPTION_EXPIRED',
  { userId: 'user123', expiredAt: '2023-10-15' }
);

Output:

Custom Error Type in TypeScript

This approach is particularly useful when working with APIs or when you need to translate error messages into different languages based on error codes.

Check out: Convert TypeScript Objects to JSON

Best Practices for TypeScript Custom Errors

When implementing custom errors, keep these tips in mind:

  1. Always fix the prototype chain with Object.setPrototypeOf() to ensure instanceof checks work correctly.
  2. Use meaningful names for your error classes that clearly indicate what went wrong.
  3. Include relevant context in your custom errors, but avoid sensitive information.
  4. Consider serialization needs if your errors will be sent over the network or logged.
  5. Create helper functions for common error-handling patterns to reduce duplication.

I hope you’ve understood how to create and use custom error types in a TypeScript project. By creating custom error types in TypeScript, we can add more context, organize our code better, and handle different scenario errors in an efficient way.

In the above methods, we have discussed all possible ways to create custom error types. From basic extensions of the Error class to patterns like error hierarchies and coded errors, you can choose from these methods based on your project’s needs.

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.