The Ultimate Guide to Error Handling in TypeScript

Mobile Developer
February 23, 2025
Updated on March 31, 2025
0 MIN READ
#security#typescript#developer-tools#performance#ultimate

Introduction

Error handling is a critical aspect of writing robust TypeScript applications. Whether you're building a backend service, a frontend app, or a full-stack solution, properly managing errors ensures your application remains stable, debuggable, and user-friendly. TypeScript enhances JavaScript's error-handling capabilities by introducing static typing, custom error types, and better tooling support.

In this guide, we'll explore best practices for error handling in TypeScript, including built-in error types, custom error classes, and advanced patterns like discriminated unions and try-catch optimizations.


Built-in Error Types in TypeScript

TypeScript inherits JavaScript's built-in error types, but with added type safety. The most common errors include:

  • Error: The base class for all errors.
  • SyntaxError: Thrown when parsing invalid code.
  • TypeError: Occurs when a variable or parameter is of an unexpected type.
  • RangeError: Triggered when a numeric value is outside valid bounds.
  • ReferenceError: Happens when accessing an undefined variable.

Here’s an example of catching a TypeError:

try { const num: number = "not a number" as any; if (isNaN(num)) { throw new TypeError("Expected a number, got a string"); } } catch (error) { if (error instanceof TypeError) { console.error("Type Error:", error.message); } else { console.error("Unexpected Error:", error); } }

Using instanceof checks ensures you handle errors safely while leveraging TypeScript’s type narrowing.


Creating Custom Error Classes

For domain-specific errors, extending the built-in Error class helps maintain consistency and improves debugging. Here’s how to define a custom ValidationError:

class ValidationError extends Error { constructor(message: string, public field?: string) { super(message); this.name = "ValidationError"; // Restore prototype chain (needed for `instanceof` checks in ES5) Object.setPrototypeOf(this, ValidationError.prototype); } } // Usage function validateUserInput(input: string) { if (!input.trim()) { throw new ValidationError("Input cannot be empty", "username"); } } try { validateUserInput(""); } catch (error) { if (error instanceof ValidationError) { console.error(`Validation failed for ${error.field}: ${error.message}`); } }

Custom errors improve clarity in logs and enable structured error handling in larger applications.


Advanced Error Handling Patterns

Discriminated Unions for Error States

TypeScript’s union types allow modeling errors as part of a function’s return type instead of throwing exceptions. This pattern, inspired by functional programming, makes error handling explicit:

type Result<T, E = Error> = | { success: true; data: T } | { success: false; error: E }; function safeParseJson(json: string): Result<unknown, SyntaxError> { try { const data = JSON.parse(json); return { success: true, data }; } catch (err) { return { success: false, error: err as SyntaxError }; } } const result = safeParseJson("{ invalid json }"); if (!result.success) { console.error("Failed to parse JSON:", result.error.message); }

This approach eliminates hidden control flow and forces callers to handle errors explicitly.

Async/Await Error Handling

When working with Promises, always wrap await calls in try-catch blocks or use .catch():

async function fetchData(url: string): Promise<Result<Response>> { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return { success: true, data: response }; } catch (error) { return { success: false, error: error as Error }; } }

For batch operations, Promise.allSettled() captures all results (successes and failures):

const promises = [fetchData("/api/1"), fetchData("/api/2")]; const results = await Promise.allSettled(promises); results.forEach((result) => { if (result.status === "fulfilled" && result.value.success) { console.log("Data:", result.value.data); } else { console.error("Error:", result.reason); } });

Conclusion

Effective error handling in TypeScript involves:

  1. Using built-in errors with type guards (instanceof).
  2. Extending Error for domain-specific cases.
  3. Adopting functional patterns like Result types for predictable control flow.
  4. Safely managing async operations with try-catch or .catch().

By applying these techniques, you’ll write more resilient applications that fail gracefully and provide actionable debugging information. For further reading, explore libraries like fp-ts for advanced functional error handling in TypeScript.

Would you like a deeper dive into any of these topics? Let us know in the comments!

Share this article