Error Handling in TypeScript: Advanced try/catch Techniques

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Advanced Error Handling in TypeScript: From try/catch to Type-Safe Solutions



Error handling is a critically important aspect of TypeScript development. Basic try/catch often proves insufficient for complex applications. In this article, we will explore advanced techniques that help make code more reliable, predictable, and type-safe.



1. The Problem with Standard try/catch: Type Loss



The main drawback of classic catch is that the error variable has type unknown (or any in older versions). This forces the developer to manually check the type:



try {  riskyOperation();} catch (error) {  // error: unknown  if (error instanceof Error) {    console.error(error.message);  } else {    console.error('Unknown error', error);  }}


Solution: creating a utility for typed error interception:



function getErrorMessage(error: unknown): string {  if (error instanceof Error) return error.message;  if (typeof error === 'string') return error;  if (error && typeof error === 'object' && 'message' in error) {    return String((error as { message: unknown }).message);  }  return 'An unknown error occurred';}

try { riskyOperation();} catch (error) { console.error(getErrorMessage(error));}


2. Custom Error Classes with Typed Data



Creating an error hierarchy allows you to precisely identify the problem and pass additional data:



// Base classclass AppError extends Error {  constructor(    message: string,    public readonly code: number,    public readonly isOperational: boolean = true  ) {    super(message);    this.name = 'AppError';  }}

// Specialized errorsclass ValidationError extends AppError { constructor( message: string, public readonly field: string, public readonly invalidValue: unknown ) { super(message, 400); this.name = 'ValidationError'; }}

class NotFoundError extends AppError { constructor(entity: string, id: string) { super(`${entity} with ID ${id} not found`, 404, false); this.name = 'NotFoundError'; }}

// Usagefunction findUser(id: string): User { if (!validateId(id)) { throw new ValidationError('Invalid ID', 'id', id); } const user = db.findUser(id); if (!user) { throw new NotFoundError('User', id); } return user;}

try { const user = findUser('123');} catch (error) { if (error instanceof NotFoundError) { // error.code === 404, error.message contains description console.warn(error.message); } else if (error instanceof ValidationError) { // error.field and error.invalidValue are available console.error(`Error in field ${error.field}: ${error.message}`); } else if (error instanceof AppError) { console.error(`Application error (code ${error.code}): ${error.message}`); }}


3. Discriminated Unions for Error Handling



This pattern allows handling operation results without exceptions, using a union type with a discriminant:



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

// Function returning Resultfunction safeDivide(a: number, b: number): Result<number, string> { if (b === 0) { return { success: false, error: 'Division by zero' }; } return { success: true, data: a / b };}

// Usageconst result = safeDivide(10, 0);

if (result.success) { // TypeScript knows that result.data is number console.log(`Result: ${result.data}`);} else { // result.error is string console.error(`Error: ${result.error}`);}

// More complex example with custom errorstype UserResult = Result<

Blogs

Book Recommendations