Error Handling in TypeScript: from try/catch to Reliable Code
TypeScript adds static typing to JavaScript, but runtime errors remain an integral part of development. Improper exception handling can lead to application crashes, data leaks, or incorrect user interface behavior. In this article, we will explore how to effectively handle errors in TypeScript using built-in mechanisms and advanced patterns.
Basic try/catch in TypeScript
The try/catch construct is the foundation of error handling. In TypeScript, unlike JavaScript, we can type the caught error. However, there is an important nuance: by default, the error type in the catch block is unknown, not Error.
try { const data = JSON.parse('invalid json'); console.log(data);} catch (error) { // error has type unknown if (error instanceof Error) { console.error('An error occurred:', error.message); } else { console.error('Unknown error:', error); }}Why is this important? In JavaScript, you can throw anything: a string, a number, an object. TypeScript protects us from unsafe access to error properties without checking. It is recommended to always check the error type before using it.
Custom Error Classes
For more precise handling, create your own error classes extending the built-in Error. This allows you to distinguish between error types and pass additional information.
class ValidationError extends Error { constructor( public field: string, message: string ) { super(message); this.name = 'ValidationError'; }}
class NetworkError extends Error { constructor( public statusCode: number, message: string ) { super(message); this.name = 'NetworkError'; }}
function processUserData(data: unknown): void { if (!data || typeof data !== 'object') { throw new ValidationError('data', 'Invalid data'); } if (!('email' in data)) { throw new ValidationError('email', 'Email is required'); }}
try { processUserData({ name: 'Ivan' });} catch (error) { if (error instanceof ValidationError) { console.error(`Validation error in field ${error.field}: ${error.message}`); } else if (error instanceof NetworkError) { console.error(`Network error ${error.statusCode}: ${error.message}`); } else { console.error('Unknown error:', error); }}This approach makes the code self-documenting and simplifies debugging. You always know where and why an error occurred.
Typing Functions with Errors: never and Discriminated Unions
TypeScript provides powerful tools for working with errors at the type level. Use the never type for functions that always throw an exception, and discriminated unions for returning errors.
function throwError(message: string): never { throw new Error(message);}
// Discriminated union for the resulttype Result<T> = | { success: true; data: T } | { success: false; error: string };
function fetchUser(id: number): Result<{ name: string }> { try { // Simulating a request if (id <= 0) { return { success: false, error: 'Invalid ID' }; } return { success: true, data: { name: 'Anna' } }; } catch (error) { return { success: false, error: 'Network error' }; }}
const result = fetchUser(1);if (result.success) { console.log('User:', result.data.name);} else { console.error('Error:', result.error);}The Result pattern allows you to avoid implicit exceptions and makes error handling