JavaScript  

What’s the Correct Way to Handle Asynchronous Errors in JavaScript Promises?

Introduction

Asynchronous programming is a core part of modern JavaScript. Promises and async/await make async code easier to write and read, but error handling can still be confusing. Many bugs happen because promise errors are not handled correctly.

In simple words, if a promise fails and you do not catch the error, your application may crash, behave unexpectedly, or fail silently. In this article, you will learn the correct way to handle asynchronous errors in JavaScript promises using clear explanations and easy examples.

What Is an Asynchronous Error in JavaScript?

An asynchronous error occurs after an async operation starts, such as a network request, file read, or timer.

Example:

fetch('/api/data')
  .then(response => response.json());

If the request fails and the error is not handled, JavaScript raises an unhandled promise rejection.

Handling Errors Using .catch()

The most basic and important rule is to always use .catch() when working with promises.

Example:

fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

Explanation:

  • .catch() handles any error in the promise chain

  • It prevents unhandled promise rejections

This is the correct way to handle errors when using .then().

Returning Promises Correctly

A common mistake is forgetting to return a promise inside .then().

Wrong example:

fetch('/api/data')
  .then(response => {
    response.json();
  })
  .catch(error => console.error(error));

Correct example:

fetch('/api/data')
  .then(response => {
    return response.json();
  })
  .then(data => console.log(data))
  .catch(error => console.error(error));

Returning the promise ensures errors propagate correctly.

Handling Errors with async/await and try-catch

The recommended modern approach is using async/await with try-catch blocks.

Example:

async function loadData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Failed to load data:', error);
  }
}

Explanation:

  • try contains async code

  • catch handles any error thrown inside try

This approach is clean, readable, and easy to debug.

Throwing Errors Inside Promises

You can throw custom errors inside promises, and they will be caught by .catch() or try-catch.

Example:

async function getUser(userId) {
  if (!userId) {
    throw new Error('User ID is required');
  }
  return { id: userId, name: 'Rahul' };
}

Thrown errors behave like rejected promises.

Handling Multiple Promises Safely

When working with multiple promises, error handling becomes more important.

Example with Promise.all:

Promise.all([fetch('/api/a'), fetch('/api/b')])
  .then(results => console.log(results))
  .catch(error => console.error('One request failed:', error));

If any promise fails, .catch() is triggered.

Avoiding Unhandled Promise Rejections

Unhandled promise rejections happen when errors are not caught.

Bad example:

async function run() {
  fetch('/api/data');
}

Good example:

async function run() {
  try {
    await fetch('/api/data');
  } catch (error) {
    console.error(error);
  }
}

Always handle promises explicitly.

Global Error Handling (Last Resort)

You can listen for unhandled promise rejections globally.

Example:

window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled promise rejection:', event.reason);
});

This should be used only as a safety net, not as the main error-handling strategy.

Comparison Table: .then/.catch vs async/await

The table below helps you quickly understand the differences between the two common approaches.

Aspect.then() / .catch()async / await
ReadabilityCan become nestedClean and readable
Error handlingUses .catch()Uses try-catch
DebuggingSlightly harderEasier
Beginner-friendlyModerateHigh
Recommended todayLess preferredYes

Real-World Examples with Fetch API and Axios

Fetch API Example

async function loadUsers() {
  try {
    const response = await fetch('/api/users');
    if (!response.ok) {
      throw new Error('Failed to fetch users');
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Fetch error:', error.message);
  }
}

Explanation:

  • Network and parsing errors are caught in one place

  • Custom errors improve clarity

Axios Example

import axios from 'axios';

async function loadPosts() {
  try {
    const response = await axios.get('/api/posts');
    console.log(response.data);
  } catch (error) {
    if (error.response) {
      console.error('Server error:', error.response.status);
    } else {
      console.error('Network error:', error.message);
    }
  }
}

Axios automatically rejects promises for non-2xx responses, making error handling simpler.

Node.js vs Browser Error Handling

Error handling behavior differs slightly between Node.js and browsers.

Browser

  • Unhandled promise rejections appear in DevTools

  • Can be caught using window.unhandledrejection

window.addEventListener('unhandledrejection', event => {
  console.error('Browser unhandled rejection:', event.reason);
});

Node.js

  • Unhandled rejections may crash the process

  • Can be handled globally

process.on('unhandledRejection', (reason) => {
  console.error('Node.js unhandled rejection:', reason);
});

In production, global handlers should log errors and allow graceful shutdown.

Testing Asynchronous Errors

Testing async error handling ensures your code behaves correctly during failures.

Example using a test framework:

async function fetchData() {
  throw new Error('API failed');
}

test('handles async error', async () => {
  await expect(fetchData()).rejects.toThrow('API failed');
});

Best practices for testing async errors:

  • Test both success and failure paths

  • Mock failed API responses

  • Ensure promises are awaited in tests

Best Practices for Promise Error Handling

To avoid async error issues:

  • Always use .catch() or try-catch

  • Return promises correctly

  • Handle errors close to where they happen

  • Log meaningful error messages

  • Avoid empty catch blocks

These practices make your JavaScript applications more stable.

Summary

The correct way to handle asynchronous errors in JavaScript promises is to always catch them using .catch() or try-catch with async/await. Properly returning promises, throwing meaningful errors, and avoiding unhandled rejections are key to building reliable applications. With consistent error-handling practices, JavaScript async code becomes easier to maintain, debug, and scale.