The Ultimate Guide to Error Handling in TypeScript
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:
- Using built-in errors with type guards (
instanceof
). - Extending
Error
for domain-specific cases. - Adopting functional patterns like
Result
types for predictable control flow. - 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!