Introduction: Why Error Handling in TypeScript Is More Than Just try/catch
TypeScript adds static typing to JavaScript, but runtime errors remain a part of any application. Unlike pure JS, where catch(e) returns any, TypeScript offers stricter mechanisms. Proper error handling is not only about preventing crashes but also about creating a clear system for debugging and code maintenance. In this article, we will cover all aspects: from basic try/catch to creating custom error hierarchies with full typing.
1. Basic try/catch Syntax and the Problem with the unknown Type
Classic Construct
The basic construct in TypeScript looks the same as in JavaScript:
try { // Code that may throw an exception const data = JSON.parse('{"name": "TypeScript"}'); console.log(data.name);} catch (error) { // Error handling console.error('An error occurred:', error);}However, there is an important difference: in TypeScript, the error variable in the catch block has the type unknown (not any as in JavaScript, unless specified otherwise). This is done for type safety.
The Problem with unknown and How to Solve It
The unknown type means we cannot simply access properties of the error, for example, error.message. The compiler will throw an error:
catch (error) { // Error TS2571: Object is of type 'unknown'. console.log(error.message); // ❌ Will not compile}To access properties, you need to narrow the type:
catch (error) { if (error instanceof Error) { console.log(error.message); // ✅ Works } else { console.log('Unknown error:', error); }}Or use a check for the existence of a property:
catch (error) { if (error && typeof error === 'object' && 'message' in error) { console.log((error as { message: string }).message); }}2. Creating Custom Error Classes with Typing
Why Is This Important?
The standard Error only contains name, message, and stack. In real projects, additional fields are often needed: error code, HTTP status, logging data. TypeScript allows you to create a hierarchy of error classes with full type support.
Example of a Custom Class
// Base class for all application errorsclass AppError extends Error { public readonly statusCode: number; public readonly isOperational: boolean;
constructor(message: string, statusCode: number = 500, isOperational: boolean = true) { super(message); this.name = 'AppError'; this.statusCode = statusCode; this.isOperational = isOperational; // Restore the prototype (necessary for TypeScript) Object.setPrototypeOf(this, new.target.prototype); }}
// Specialized errorsclass NotFoundError extends AppError { constructor(resource: string) { super(`${resource} not found`, 404); this.name = 'NotFoundError'; }}
class ValidationError extends AppError { public readonly errors: string[];
constructor(errors: string[]) { super('Validation error', 400); this.name = 'ValidationError'; this.errors = errors; }}Usage with try/catch
function getUser(id: number): User { if (id <= 0) { throw new ValidationError(['ID must be a positive number']); } // ... logic for getting the user if (!user) { throw new NotFoundError('User'); } return user;}
try { const user = getUser(-1);} catch (error) { if (err