Sign in
Topics
This blog has provided a solid understanding of TypeScript's error-handling features. You've learned how to throw errors in TypeScript functions and how to handle them using the try...catch statement. You also know how to avoid common mistakes and handle errors effectively.
Struggling to make sense of cryptic try-catch errors in TypeScript? Many developers run into issues like unclear error messages, lost stack traces, or unexpected runtime behavior, especially in larger applications or API-heavy projects. This guide is for TypeScript developers who want cleaner, safer, and more reliable error handling. We’ll break down the most common pitfalls and show how to avoid them using real-world patterns and tools. By the end, you’ll write code that’s easier to debug, maintain, and trust in production.
Let’s break down the most frequent mistakes developers make and how to avoid them with better try-catch patterns, safer error object handling, and smarter type checking.
Throwing a string or number feels quick, but it’s a trap.
throw "Something went wrong"; // ❌ Bad Practice
This throw statement
doesn’t produce a proper stack trace
or usable error object
. The right way is to always throw an instance of the built-in Error class
or your own custom error class
.
throw new Error("Something went wrong"); // ✅ Good Practice
To avoid this:
useUnknownInCatchVariables
in tsconfig.json
.typescript-eslint
's no-throw-literal
rule.Key Tip: Use a utility like ensureError() to safely normalize unknown catch block values.
Consider the above example:
1try { 2 doSomething(); 3} catch (err) { 4 throw new Error("Something failed"); // ❌ Lost context 5} 6
We lost the original stack trace. Use the cause property to preserve it.
1try { 2 doSomething(); 3} catch (err) { 4 throw new Error("Something failed", { cause: err }); // ✅ 5} 6
Supported in Node.js 16.9+ and major browsers, this retains the full call stack.
Changing error messages dynamically (with user or API data) makes monitoring harder.
throw new Error(`Customer ${customerId} failed to register`); // ❌
Instead, use constant messages and attach context separately:
throw new CustomError("CUSTOMER_REGISTRATION_FAILED", { customerId }); // ✅
This helps tools like Sentry group errors effectively.
You’ll need your custom error class to handle specific business logic or provide structured context.
1export class CustomError extends Error { 2 constructor(public code: string, public context: Record<string, unknown>) { 3 super(code); 4 this.name = "CustomError"; 5 } 6} 7
1throw new CustomError("USER_NOT_FOUND", { userId: "1234" }); 2
Then, in your catch block, you can check the error type:
1try { 2 getUser(); 3} catch (e) { 4 if (e instanceof CustomError) { 5 console.error(e.code, e.context); 6 } 7} 8
Use the instanceof operator to narrow the error type and provide specific recovery actions.
Here’s a proper try-catch structure that preserves context and logs effectively:
1function validatePassword(password: string): void { 2 try { 3 if (password.length < 8) { 4 throw new Error("Password too short"); 5 } 6 } catch (error: unknown) { 7 if (error instanceof Error) { 8 console.error("Validation failed:", error.message); // `error.message` is safe here 9 } else { 10 console.error("Unexpected error:", error); 11 } 12 } 13} 14
Important: Always perform type checking on the error parameter when using catch statement.
Sometimes, you expect failures—like a network request timeout or validation issue. Don’t throw an error. Return a result type instead.
1import { ok, err, Result } from 'ts-results'; 2 3function fetchData(): Result<string, Error> { 4 if (Math.random() < 0.5) { 5 return ok("Data loaded"); 6 } 7 return err(new Error("Network error")); 8} 9
This pattern avoids runtime exceptions, making your code predictable.
Here’s how to handle catch blocks properly.
1try { 2 riskyOperation(); 3} catch (error: unknown) { 4 if (error instanceof Error) { 5 console.error(error.message); // Safe to access 6 } 7} 8
1try { 2 riskyOperation(); 3} catch (error) { 4 console.error(error.nonExistentProperty); // TypeScript allows it, but risky! 5} 6
Use typeof error === 'object'
as an extra guard if needed, especially when error could be anything.
Use custom type guards to safely identify specific error shapes:
1function isCustomError(error: unknown): error is CustomError { 2 return error instanceof CustomError && typeof error.code === 'string'; 3} 4
This helps in precise error handling:
1catch (error: unknown) { 2 if (isCustomError(error)) { 3 log(error.code, error.context); 4 } 5} 6
Pitfall | Why it’s a Problem | Fix |
---|---|---|
Throwing literals | No stack trace or context | Use new Error() |
Overwriting error | Lost original context | Use cause |
Dynamic messages | Hard to group in monitoring | Use constants and context |
Missing type checks | Unsafe access to error.message | Use instanceof and type guards |
Type | Use Case | Notes |
---|---|---|
Basic Try-Catch | Handle runtime errors | Always check the error type |
Try-Catch with Cause | Preserve error chains | Use in nested failure scenarios |
Result Pattern | Expected failures | Avoids exceptions, improves control flow |
Modern TypeScript error handling combines the power of static typing with well-structured try-catch patterns and functional tools like the Result type. By using proper catch blocks, defining robust custom error classes, preserving stack traces, and avoiding dynamic error messages, developers can write more predictable, maintainable, and debuggable code. TypeScript 5.8 may not introduce new error features, but its compiler optimizations improve the overall developer experience. Avoid these common pitfalls and follow the patterns shared here to build resilient applications with confidence.