Exception Handling in TypeScript

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!

What is Exception Handling in TypeScript?

TypeScript, being a superset of JavaScript, inherits its exception handling mechanisms but adds static type-checking that can help prevent errors before they occur.

Exception handling is all about anticipating potential errors in your code and managing them gracefully, rather than letting them crash your application.

Exception Handling in TypeScript: Different Methods Explained

Below, I will explain the various methods for handling exceptions in TypeScript.

Method 1: Using Try/Catch Blocks in TypeScript

The most fundamental way to handle exceptions in TypeScript is by using try/catch blocks. This approach allows you to capture runtime errors and respond appropriately.

Here’s how to implement basic exception handling:

function divideNumbers(a: number, b: number): number {
  try {
    if (b === 0) {
      throw new Error("Cannot divide by zero!");
    }
    return a / b;
  } catch (error) {
    console.error("An error occurred:", error.message);
    // Return a fallback value or rethrow the error
    return Infinity; // Returning infinity as a fallback
  } finally {
    console.log("Division operation attempted");
    // This block always executes, regardless of whether an exception occurred
  }
}

// Usage
console.log(divideNumbers(10, 2)); // 5
console.log(divideNumbers(10, 0)); // Infinity

The try block contains code that might throw an exception, the catch block handles any errors, and the finally block contains code that always executes.

Exception Handling in TypeScript

Method 2: Creating Custom Error Types in TypeScript

For more sophisticated applications, creating custom error types can help distinguish between different error scenarios and provide more context.

// Define custom error classes
class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ValidationError";
    // This maintains proper stack traces in Node.js
    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);
  }
}

// Usage
function processUserData(userData: any): void {
  try {
    if (!userData.name) {
      throw new ValidationError("Name is required");
    }

    // Simulate database error
    if (Math.random() < 0.5) {
      throw new DatabaseError("Connection failed", 500);
    }

    console.log("Data processed successfully");
  } catch (error) {
    if (error instanceof ValidationError) {
      console.error("Validation issue:", error.message);
      // Handle validation errors
    } else if (error instanceof DatabaseError) {
      console.error(`Database error (${error.code}):`, error.message);
      // Handle database errors, possibly retry
    } else {
      console.error("Unknown error:", error);
      // Handle other unexpected errors
    }
  }
}

This approach helps organize error handling in larger codebases and provides more informative error messages.

Custom Error Types in TypeScript

Method 3: Handling Async Exceptions with Try/Catch in TypeScript

When working with asynchronous code (which is common in TypeScript applications), you need to handle exceptions in promises and async/await functions.

// Using async/await with try/catch and public API
async function fetchUserData(userId: string): Promise<any | null> {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    if (error instanceof Error) {
      console.error("Failed to fetch user data:", error.message);
    } else {
      console.error("An unknown error occurred while fetching user data.");
    }
    return null;
  }
}

// Usage
async function displayUserProfile(userId: string): Promise<void> {
  const userData = await fetchUserData(userId);

  if (userData) {
    console.log(`User Name: ${userData.name}`);
    console.log(`Email: ${userData.email}`);
    console.log(`City: ${userData.address.city}`);
  } else {
    console.log("Could not load user profile. Please try again.");
  }
}

// Call the function
displayUserProfile("1"); // Try "2", "10", or an invalid ID like "100" to test
Handle Async Exceptions with Try-Catch in TypeScript

Method 4: Using Error Boundaries in React TypeScript Applications

If you’re working with React and TypeScript, error boundaries provide a way to catch JavaScript errors anywhere in a component tree and display a fallback UI.

// ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  fallback?: ReactNode;
  children?: ReactNode;
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
}

interface State {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
    if (this.props.onError) {
      this.props.onError(error, errorInfo);
    }
    console.error("Error caught by boundary:", error, errorInfo);
  }

  render(): ReactNode {
    if (this.state.hasError) {
      return this.props.fallback ?? (
        <div className="error-container">
          <h2>Something went wrong.</h2>
          <details>
            <summary>Error Details</summary>
            <p>{this.state.error?.toString()}</p>
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Usage:

// App.tsx
import React from 'react';
import ErrorBoundary from './ErrorBoundary';
import UserProfile from './UserProfile';

function App() {
  return (
    <div className="app">
      <h1>My TypeScript App</h1>
      <ErrorBoundary 
        fallback={<p>Could not load user profile. Please try again later.</p>}
        onError={(error) => {
          // Send error to monitoring service
          console.error("Logged to service:", error);
        }}
      >
        <UserProfile userId="123" />
      </ErrorBoundary>
    </div>
  );
}

export default App;

Output:

Error Boundaries in React TypeScript Applications

Conclusion

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.

I hope you found this article helpful. If you have any questions or suggestions, kindly leave them in the comments below. Remember, proper exception handling isn’t just about preventing crashes – it’s about creating a better user experience and making your code more maintainable.

Other TypeScript articles you may also like:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.