Good error handling makes code robust and debuggable. Here's how to handle errors elegantly in TypeScript.
Custom Error Classes#
1// Base application error
2class AppError extends Error {
3 constructor(
4 message: string,
5 public code: string,
6 public statusCode: number = 500,
7 public isOperational: boolean = true
8 ) {
9 super(message);
10 this.name = this.constructor.name;
11 Error.captureStackTrace(this, this.constructor);
12 }
13}
14
15// Specific error types
16class ValidationError extends AppError {
17 constructor(message: string, public fields?: Record<string, string>) {
18 super(message, 'VALIDATION_ERROR', 400);
19 }
20}
21
22class NotFoundError extends AppError {
23 constructor(resource: string) {
24 super(`${resource} not found`, 'NOT_FOUND', 404);
25 }
26}
27
28class UnauthorizedError extends AppError {
29 constructor(message = 'Unauthorized') {
30 super(message, 'UNAUTHORIZED', 401);
31 }
32}
33
34// Usage
35function getUser(id: string): User {
36 const user = db.users.find(u => u.id === id);
37 if (!user) {
38 throw new NotFoundError('User');
39 }
40 return user;
41}
42
43// Type guard for error handling
44function isAppError(error: unknown): error is AppError {
45 return error instanceof AppError;
46}
47
48try {
49 const user = getUser('123');
50} catch (error) {
51 if (isAppError(error)) {
52 console.log(error.code, error.statusCode);
53 }
54}Result Type Pattern#
1// Result type for explicit error handling
2type Result<T, E = Error> =
3 | { success: true; data: T }
4 | { success: false; error: E };
5
6// Helper functions
7function ok<T>(data: T): Result<T, never> {
8 return { success: true, data };
9}
10
11function err<E>(error: E): Result<never, E> {
12 return { success: false, error };
13}
14
15// Usage
16interface UserError {
17 code: 'NOT_FOUND' | 'INVALID_EMAIL' | 'DUPLICATE';
18 message: string;
19}
20
21async function createUser(data: CreateUserInput): Promise<Result<User, UserError>> {
22 // Validate email
23 if (!isValidEmail(data.email)) {
24 return err({ code: 'INVALID_EMAIL', message: 'Invalid email format' });
25 }
26
27 // Check for duplicate
28 const existing = await db.user.findUnique({ where: { email: data.email } });
29 if (existing) {
30 return err({ code: 'DUPLICATE', message: 'Email already exists' });
31 }
32
33 const user = await db.user.create({ data });
34 return ok(user);
35}
36
37// Caller handles explicitly
38async function handleCreateUser(data: CreateUserInput) {
39 const result = await createUser(data);
40
41 if (!result.success) {
42 switch (result.error.code) {
43 case 'INVALID_EMAIL':
44 return res.status(400).json({ error: result.error.message });
45 case 'DUPLICATE':
46 return res.status(409).json({ error: result.error.message });
47 default:
48 return res.status(500).json({ error: 'Unknown error' });
49 }
50 }
51
52 return res.status(201).json(result.data);
53}Option/Maybe Type#
1// Option type for nullable values
2type Option<T> = { some: true; value: T } | { some: false };
3
4function some<T>(value: T): Option<T> {
5 return { some: true, value };
6}
7
8function none<T>(): Option<T> {
9 return { some: false };
10}
11
12// Helper methods
13const Option = {
14 map<T, U>(opt: Option<T>, fn: (value: T) => U): Option<U> {
15 return opt.some ? some(fn(opt.value)) : none();
16 },
17
18 flatMap<T, U>(opt: Option<T>, fn: (value: T) => Option<U>): Option<U> {
19 return opt.some ? fn(opt.value) : none();
20 },
21
22 getOrElse<T>(opt: Option<T>, defaultValue: T): T {
23 return opt.some ? opt.value : defaultValue;
24 },
25
26 getOrThrow<T>(opt: Option<T>, error: Error): T {
27 if (!opt.some) throw error;
28 return opt.value;
29 },
30};
31
32// Usage
33function findUser(id: string): Option<User> {
34 const user = users.find(u => u.id === id);
35 return user ? some(user) : none();
36}
37
38const user = findUser('123');
39const name = Option.map(user, u => u.name);
40const displayName = Option.getOrElse(name, 'Anonymous');Try-Catch Utilities#
1// Async try-catch wrapper
2async function tryCatch<T>(
3 promise: Promise<T>
4): Promise<[T, null] | [null, Error]> {
5 try {
6 const data = await promise;
7 return [data, null];
8 } catch (error) {
9 return [null, error as Error];
10 }
11}
12
13// Usage
14const [user, error] = await tryCatch(fetchUser(id));
15
16if (error) {
17 console.error('Failed to fetch user:', error);
18 return;
19}
20
21console.log('User:', user.name);
22
23// Sync version
24function tryCatchSync<T>(fn: () => T): [T, null] | [null, Error] {
25 try {
26 return [fn(), null];
27 } catch (error) {
28 return [null, error as Error];
29 }
30}
31
32// With error transformation
33async function tryCatchMap<T, E>(
34 promise: Promise<T>,
35 mapError: (error: unknown) => E
36): Promise<[T, null] | [null, E]> {
37 try {
38 const data = await promise;
39 return [data, null];
40 } catch (error) {
41 return [null, mapError(error)];
42 }
43}Error Boundary Pattern#
1// For React components
2import { Component, ReactNode } from 'react';
3
4interface Props {
5 children: ReactNode;
6 fallback: ReactNode;
7 onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
8}
9
10interface State {
11 hasError: boolean;
12 error: Error | null;
13}
14
15class ErrorBoundary extends Component<Props, State> {
16 state: State = { hasError: false, error: null };
17
18 static getDerivedStateFromError(error: Error): State {
19 return { hasError: true, error };
20 }
21
22 componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
23 this.props.onError?.(error, errorInfo);
24
25 // Log to error tracking service
26 logError(error, errorInfo);
27 }
28
29 render() {
30 if (this.state.hasError) {
31 return this.props.fallback;
32 }
33
34 return this.props.children;
35 }
36}
37
38// Usage
39function App() {
40 return (
41 <ErrorBoundary
42 fallback={<ErrorPage />}
43 onError={(error) => console.error('Caught:', error)}
44 >
45 <MainContent />
46 </ErrorBoundary>
47 );
48}Assertion Functions#
1// Type narrowing with assertions
2function assertDefined<T>(
3 value: T | undefined | null,
4 message?: string
5): asserts value is T {
6 if (value === undefined || value === null) {
7 throw new Error(message ?? 'Value is not defined');
8 }
9}
10
11function assertNever(value: never): never {
12 throw new Error(`Unexpected value: ${value}`);
13}
14
15// Usage
16function processStatus(status: 'pending' | 'active' | 'completed') {
17 switch (status) {
18 case 'pending':
19 return 'Waiting...';
20 case 'active':
21 return 'In progress';
22 case 'completed':
23 return 'Done';
24 default:
25 assertNever(status); // TypeScript error if case missing
26 }
27}
28
29// With type guards
30function assertIsUser(value: unknown): asserts value is User {
31 if (typeof value !== 'object' || value === null) {
32 throw new Error('Not an object');
33 }
34 if (!('id' in value) || !('email' in value)) {
35 throw new Error('Missing required properties');
36 }
37}
38
39const data: unknown = await fetchData();
40assertIsUser(data);
41// data is now typed as User
42console.log(data.email);Error Aggregation#
1// Collect multiple errors
2class AggregateError extends Error {
3 constructor(public errors: Error[], message?: string) {
4 super(message ?? `${errors.length} errors occurred`);
5 this.name = 'AggregateError';
6 }
7}
8
9async function validateAll(
10 validators: Array<() => Promise<void>>
11): Promise<void> {
12 const results = await Promise.allSettled(validators.map(v => v()));
13
14 const errors = results
15 .filter((r): r is PromiseRejectedResult => r.status === 'rejected')
16 .map(r => r.reason);
17
18 if (errors.length > 0) {
19 throw new AggregateError(errors);
20 }
21}
22
23// Usage
24try {
25 await validateAll([
26 () => validateEmail(email),
27 () => validatePassword(password),
28 () => checkDuplicate(email),
29 ]);
30} catch (error) {
31 if (error instanceof AggregateError) {
32 error.errors.forEach(e => console.log(e.message));
33 }
34}Best Practices#
Design:
✓ Use custom error classes
✓ Consider Result types for expected failures
✓ Make errors descriptive
✓ Include error codes
Handling:
✓ Catch at appropriate levels
✓ Log with context
✓ Don't swallow errors silently
✓ Transform errors for different layers
TypeScript:
✓ Use type guards
✓ Leverage discriminated unions
✓ Use assertion functions
✓ Handle unknown type properly
Conclusion#
TypeScript enables robust error handling patterns. Use custom error classes for operational errors, Result types for expected failures, and proper type guards for safe error handling. Choose patterns based on whether errors are expected or exceptional.