Handling API Response Errors Gracefully in TypeScript Projects

Learn how to handle API response errors gracefully in TypeScript projects with simple techniques to improve reliability and user experience.

When working with APIs in TypeScript, handling errors properly is crucial for building reliable and user-friendly applications. Instead of letting your app crash or behave unpredictably when an API call fails, you can catch and manage these errors gracefully to provide meaningful feedback to users.

In this guide, we'll cover basic strategies to handle API response errors in TypeScript using async/await, try/catch blocks, and custom error types. By the end, you'll be able to write cleaner and safer code that anticipates and handles failures effectively.

First, let's look at how to call an API using fetch with proper error handling.

typescript
async function fetchData(url: string): Promise<any> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      // Throw an error if the HTTP status is not in the 200-299 range
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    // This will catch network errors or thrown HTTP errors
    throw new Error(`Failed to fetch data: ${(error as Error).message}`);
  }
}

In the code above, we check the `response.ok` property to determine if the HTTP request was successful. If not, we throw an error with the status code. We then catch any errors and rethrow them with a descriptive message.

Next, let's define a custom error type to handle different API error scenarios more clearly:

typescript
class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'ApiError';
  }
}

async function fetchDataCustomError(url: string): Promise<any> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new ApiError(response.status, `API responded with status ${response.status}`);
    }
    return response.json();
  } catch (error) {
    if (error instanceof ApiError) {
      // Handle known API errors
      console.error(`API Error (${error.status}): ${error.message}`);
    } else {
      // Handle unexpected errors
      console.error(`Unexpected error: ${(error as Error).message}`);
    }
    throw error; // Re-throw error to be handled by caller
  }
}

Using a custom error class like `ApiError` helps to distinguish between different error types, allowing you to respond accordingly, for example, by showing specific messages or triggering retries.

Finally, let's see how you might call this function and handle errors at a higher level in your app:

typescript
async function main() {
  const apiUrl = 'https://api.example.com/data';

  try {
    const data = await fetchDataCustomError(apiUrl);
    console.log('Data received:', data);
  } catch (error) {
    if (error instanceof ApiError) {
      alert(`Oops! There was a problem fetching data (status: ${error.status}). Please try again later.`);
    } else {
      alert('An unexpected error occurred. Please try again.');
    }
  }
}

main();

By catching errors in your main functional blocks, you can display user-friendly messages or take recovery actions, improving the overall user experience.

In summary, handling API response errors gracefully in TypeScript involves checking response status codes, using try/catch blocks, and optionally creating custom error classes. These practices help prevent crashes, provide better feedback to users, and make your code easier to maintain.