Harnessing TypeScript's Advanced Type Guards for Robust Error Handling

Learn how to use TypeScript's advanced type guards to create safer and more reliable error handling in your applications.

Error handling is a crucial part of developing robust applications. In TypeScript, one powerful way to handle errors effectively is by using advanced type guards. Type guards help you narrow down the type of a value at runtime, providing better safety and enabling smarter handling of different error types.

Let's start with a simple example. Imagine you have a function that can throw different kinds of errors, and you want to handle them differently based on their type.

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

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

function mightThrowError(shouldThrow: boolean) {
  if (!shouldThrow) return 'Success!';
  const errorType = Math.random() > 0.5 ? 'network' : 'validation';
  if (errorType === 'network') {
    throw new NetworkError('Failed to connect.');
  } else {
    throw new ValidationError('Invalid input.');
  }
}

Now, in your error handling code, you want to differentiate between these errors. Using TypeScript’s `instanceof` operator, you can create a custom type guard.

typescript
function isNetworkError(error: unknown): error is NetworkError {
  return error instanceof NetworkError;
}

function isValidationError(error: unknown): error is ValidationError {
  return error instanceof ValidationError;
}

These type guard functions tell TypeScript exactly what type the error is during conditional checks. Let's see how to use these in a try-catch block:

typescript
try {
  const result = mightThrowError(true);
  console.log(result);
} catch (error) {
  if (isNetworkError(error)) {
    console.error('Network error occurred:', error.message);
  } else if (isValidationError(error)) {
    console.error('Validation error occurred:', error.message);
  } else {
    console.error('An unknown error occurred:', error);
  }
}

In this setup, TypeScript understands within each block what type the error has, so you get full type safety and autocompletion benefits. This makes your error handling code more robust and less prone to mistakes.

You can also use more advanced techniques like checking for custom properties or discriminated unions if your error objects have specific distinguishing fields.

typescript
interface ApiError {
  code: number;
  message: string;
  type: 'api';
}

interface ClientError {
  message: string;
  type: 'client';
}

function isApiError(error: any): error is ApiError {
  return error?.type === 'api' && typeof error?.code === 'number';
}

function isClientError(error: any): error is ClientError {
  return error?.type === 'client';
}

Using these advanced type guards helps you handle various errors precisely and allows your application to react appropriately, improving user experience and maintainability.

In summary, by harnessing TypeScript’s advanced type guards, you can write safer, clearer, and more robust error handling code that helps catch issues early and handle them gracefully.