While developing a TypeScript-based web application, you created a feature that fetches user data from an API. Sometimes, the API is down or returns an error. To handle such cases without crashing the app, you used a try-catch block inside an async function.
This way, if the API call fails, you can display an error message, such as “Something went wrong, please try again later,” instead of a blank screen or a broken app.
In this article, I’ll cover several effective ways of exception handling in TypeScript (including basic try/catch, custom error types, and async error handling). So let’s dive in!
Understanding Exceptions in TypeScript
TypeScript, like JavaScript, allows you to throw and catch exceptions using the familiar try-catch pattern. However, TypeScript introduces powerful type-checking capabilities that enhance the robustness of exception handling.
Let’s start with the basics and then explore more advanced patterns.
Basic Exception Throwing in TypeScript
The simplest way to throw an exception in TypeScript is to use the throw keyword:
function validateAge(age: number): void {
if (age < 0) {
throw new Error("Age cannot be negative");
}
if (age > 120) {
throw new Error("Age seems unrealistically high");
}
console.log("Age is valid:", age);
}
try {
validateAge(150);
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error(error);
}
}Here, I’m validating a user’s age for a US healthcare application. If the age is outside reasonable bounds, an exception is thrown with a descriptive message.

Custom Error Types in TypeScript
While using the built-in Error class works fine, I’ve found that creating custom error types makes exception handling much more maintainable, especially in larger applications:
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
// Necessary for proper inheritance in TypeScript
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
class DatabaseError extends Error {
constructor(message: string, public readonly code: number) {
super(message);
this.name = "DatabaseError";
Object.setPrototypeOf(this, DatabaseError.prototype);
}
}
function processUserData(userId: string): void {
if (!userId) {
throw new ValidationError("User ID is required");
}
// Simulate database error
if (userId === "invalid") {
throw new DatabaseError("Failed to fetch user data", 404);
}
console.log("Processing data for user:", userId);
}
// Test call inside try-catch
try {
processUserData("invalid"); // Change this to "", "invalid", or a valid ID to test
} catch (error) {
if (error instanceof ValidationError) {
console.error("Validation Error:", error.message);
} else if (error instanceof DatabaseError) {
console.error(`Database Error (${error.code}):`, error.message);
} else if (error instanceof Error) {
console.error("General Error:", error.message);
} else {
console.error("Unknown error:", error);
}
}
By creating specific error types, I can include additional contextual information (like error codes) and handle different error categories separately.

Handling Different Types of Exceptions in TypeScript
One of TypeScript’s strengths is its ability to use type checking when catching exceptions:
// Define ValidationError class
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
// Define DatabaseError class
class DatabaseError extends Error {
constructor(message: string, public readonly code: number) {
super(message);
this.name = "DatabaseError";
Object.setPrototypeOf(this, DatabaseError.prototype);
}
}
// Define processUserData function
function processUserData(userId: string): void {
if (!userId) {
throw new ValidationError("User ID is required");
}
if (userId === "invalid") {
throw new DatabaseError("Failed to fetch user data", 404);
}
console.log("Processing data for user:", userId);
}
// Now safely call with try-catch
try {
processUserData("invalid"); // test with "", "invalid", or "user123"
} catch (error) {
if (error instanceof ValidationError) {
console.log("Invalid input:", error.message);
} else if (error instanceof DatabaseError) {
console.log(`Database error (${error.code}):`, error.message);
} else if (error instanceof Error) {
console.log("General error:", error.message);
} else {
console.log("Unknown error:", error);
}
}
This approach enables you to handle different error categories appropriately, which is critical for real-world applications where various things can go wrong.

Async Exception Handling in TypeScript
Most modern TypeScript applications use asynchronous code. Here’s how to handle exceptions in async functions:
// Custom error classes
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
class DatabaseError extends Error {
constructor(message: string, public readonly code: number) {
super(message);
this.name = "DatabaseError";
Object.setPrototypeOf(this, DatabaseError.prototype);
}
}
// Async function to simulate fetching user profile
async function fetchUserProfile(userId: string): Promise<any> {
if (!userId) {
throw new ValidationError("User ID is required");
}
try {
// Simulate API call and possible error
if (userId === "invalid") {
throw new DatabaseError("User not found", 404);
}
// Simulate successful API response
return { id: userId, name: "John Doe" };
} catch (error) {
if (error instanceof Error) {
console.log("Error in fetchUserProfile:", error.message);
} else {
console.log("Unknown error in fetchUserProfile:", error);
}
throw error; // rethrow for caller to handle
}
}
// Usage function to display user profile
async function displayUserProfile(userId: string): Promise<void> {
try {
const profile = await fetchUserProfile(userId);
console.log("User profile:", profile);
} catch (error) {
if (error instanceof ValidationError) {
console.log("Invalid input:", error.message);
} else if (error instanceof DatabaseError) {
console.log(`Database error (${error.code}):`, error.message);
} else if (error instanceof Error) {
console.log("General error:", error.message);
} else {
console.log("Unknown error:", error);
}
}
}
// Test calls
displayUserProfile(""); // Test with missing userId
displayUserProfile("invalid"); // Test with invalid userId
displayUserProfile("user123"); // Test with valid userId
In this pattern, I’m re-throwing the exception from the inner function, allowing the caller to decide how to handle different error types.

Best Practices for Exception Handling in TypeScript
After years of working with TypeScript, I’ve developed these best practices:
1. Be Specific About What You Throw
Rather than throwing generic errors, create specific error types that relate to your domain:
// Instead of this:
throw new Error("Invalid tax ID");
// Do this:
throw new TaxValidationError("Tax ID format is invalid for US resident");2. Only Catch What You Can Handle
Don’t catch exceptions unless you can meaningfully handle them:
try {
processPayment(order);
} catch (error) {
if (error instanceof PaymentDeclinedError) {
notifyUser("Your payment was declined. Please try another card.");
} else {
// Re-throw errors we don't know how to handle
throw error;
}
}3. Include Contextual Information
Make your error messages informative:
function validateZipCode(zipCode: string): void {
if (!/^\d{5}(-\d{4})?$/.test(zipCode)) {
throw new ValidationError(
`"${zipCode}" is not a valid US ZIP code. Format should be 12345 or 12345-6789.`
);
}
}Advanced Pattern: Result Type in TypeScript
For critical operations where you want to avoid exceptions entirely, consider using a Result type pattern:
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
function divideNumbers(a: number, b: number): Result<number> {
if (b === 0) {
return {
success: false,
error: new Error("Cannot divide by zero")
};
}
return {
success: true,
value: a / b
};
}
// Usage
const result = divideNumbers(10, 2);
if (result.success) {
console.log("Result:", result.value);
} else {
console.log("Error:", result.error.message);
}This pattern is particularly useful in functional programming approaches and when working with complex operations where multiple errors might occur.

Proper exception handling in TypeScript is essential for building robust applications. By utilizing custom error types, handling exceptions effectively, and adhering to best practices, you can develop more reliable and maintainable code.
Conclusion
I hope you found this guide helpful. Both methods work great – the try/catch approach is ideal for localized error handling, while custom error types provide better organization for larger applications.
Global error handling ensures that no exception goes unnoticed, and React error boundaries provide elegant UI fallbacks.
Other TypeScript articles you may also like:
- TypeScript let vs var Difference
- Type vs Interface in TypeScript
- TypeScript: Check If a Key Exists in an Object

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.