Error Handling in Asynchronous Code in JavaScript

Handling errors in asynchronous code is crucial for building stable, reliable applications. When dealing with asynchronous tasks like network requests, file reads, or database calls, errors can occur due to network issues, invalid data, or other unexpected conditions. In JavaScript, error handling differs depending on whether you’re using callbacks, promises, or async/await. This chapter covers best practices for error handling in asynchronous code, with examples demonstrating how to manage errors gracefully across different asynchronous programming techniques.


1. Error Handling with Callbacks

With callbacks, errors are often handled by passing an error object or message as the first argument in the callback function. This approach allows the function receiving the callback to check for an error and handle it accordingly.

Example of Error Handling in Callbacks

function fetchData(callback) {
setTimeout(() => {
const error = Math.random() > 0.5 ? "Network error" : null;
const data = error ? null : "Fetched Data";

callback(error, data);
}, 1000);
}

fetchData((error, data) => {
if (error) {
console.error("Error:", error);
} else {
console.log("Data received:", data);
}
});

In this example, fetchData simulates a network request that may succeed or fail. The callback receives an error and data argument, allowing it to handle errors accordingly.

Best Practices for Callback Error Handling

  • Always check for errors: The callback function should always check if an error exists before proceeding.
  • Use descriptive error messages: Make sure error messages are clear to simplify debugging.

2. Error Handling with Promises

Promises provide a built-in .catch() method for error handling. If a promise is rejected, the .catch() method allows you to handle the error without blocking other code. Promises also support chained error handling by adding .catch() at the end of a chain.

Example of Error Handling with Promises

function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const error = Math.random() > 0.5;
if (error) {
reject("Network error");
} else {
resolve("Fetched Data");
}
}, 1000);
});
}

fetchData()
.then((data) => {
console.log("Data received:", data);
})
.catch((error) => {
console.error("Error:", error);
});

In this example, if fetchData encounters an error, it rejects the promise, which is then handled by .catch().

Chained Error Handling

When chaining promises, adding a .catch() at the end handles any error that occurs in any part of the chain.

fetchData()
.then((data) => processData(data))
.then((processedData) => saveData(processedData))
.catch((error) => {
console.error("Error occurred in the chain:", error);
});

Here, any error in the chain is caught by the final .catch().


3. Error Handling with Async/Await

With async/await, errors are caught using try...catch blocks. This approach keeps error handling close to the code where the error might occur, making it easier to read and understand.

Example of Error Handling with Async/Await

function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const error = Math.random() > 0.5;
if (error) {
reject("Network error");
} else {
resolve("Fetched Data");
}
}, 1000);
});
}

async function getData() {
try {
const data = await fetchData();
console.log("Data received:", data);
} catch (error) {
console.error("Error:", error);
}
}

getData();

In this example, getData uses await to pause until fetchData completes. If fetchData throws an error, it’s caught in the catch block.

Chaining Async/Await Functions with Error Handling

For multiple async functions, use try...catch within each function, or use a single try...catch block around multiple await statements.

async function main() {
try {
const data = await fetchData();
const processedData = await processData(data);
await saveData(processedData);
} catch (error) {
console.error("Error in async chain:", error);
}
}

main();

In this example, any error from fetchData, processData, or saveData is caught in the catch block.


4. Handling Errors in Promise.all and Promise.race

When working with Promise combinators like Promise.all or Promise.race, error handling can be slightly different.

Error Handling with Promise.all

In Promise.all, if any promise rejects, the entire operation fails, and the error is caught in .catch().

const p1 = Promise.resolve("First");
const p2 = Promise.reject("Second failed");
const p3 = Promise.resolve("Third");

Promise.all([p1, p2, p3])
.then((results) => {
console.log("All results:", results);
})
.catch((error) => {
console.error("Error in Promise.all:", error);
});

In this example, if any promise (like p2) rejects, Promise.all rejects with that error, even if other promises succeed.

Error Handling with Promise.race

With Promise.race, only the first settled promise (resolved or rejected) determines the outcome. If the first promise to settle is an error, it’s caught in .catch().

const slow = new Promise((resolve) => setTimeout(resolve, 2000, "Slow success"));
const fast = new Promise((_, reject) => setTimeout(reject, 1000, "Fast failure"));

Promise.race([slow, fast])
.then((result) => {
console.log("Race result:", result);
})
.catch((error) => {
console.error("Error in Promise.race:", error);
});

Here, Promise.race will reject with “Fast failure” because fast is the first to settle.


5. Best Practices for Error Handling in Async Code

  • Log meaningful error messages: Ensure error messages provide useful context for debugging.
  • Centralize error handling: Use a final .catch() in promise chains or a single try...catch for multiple async calls.
  • Retry logic: For recoverable errors (e.g., network errors), consider implementing retry logic.
  • Graceful degradation: When possible, handle errors in a way that provides fallback data or continues limited functionality.

Exercises


  1. Basic Error Handling with Callbacks
    • Objective: Practice error handling with callbacks.
    • Instructions: Write a function loadData that simulates a network request. Pass an error to the callback if the request fails. Call loadData with a callback that handles success and failure cases.
  2. Promise-Based Error Handling
    • Objective: Practice using .catch() to handle promise errors.
    • Instructions: Write a function fetchUserData that returns a promise. If the promise rejects, catch the error using .catch() and log an error message.
  3. Using Try-Catch with Async/Await
    • Objective: Handle errors in async/await code.
    • Instructions: Write an async function getProfile that uses await to fetch user data. Use try...catch to log an error message if data fetching fails.
  4. Error Handling in Promise.all
    • Objective: Handle errors with Promise.all.
    • Instructions: Write three promises, where one rejects. Use Promise.all to retrieve the data, and handle the error with .catch() if any promise rejects.
  5. Error Handling with Timeout and Promise.race
    • Objective: Use Promise.race to handle timeouts.
    • Instructions: Write a function that returns a promise resolving after 2 seconds. Race it against a 1-second timeout promise, and log a timeout error if the timeout wins.

Multiple-Choice Questions


  1. How do you handle errors in a promise chain?
    • A) Using a try...catch block
    • B) Using a catch() method at the end of the chain
    • C) By ignoring them
    • D) By nesting promises inside a callback
    Answer: B. Errors in a promise chain are handled using .catch() at the end of the chain.
  2. What does try...catch do in async/await code?
    • A) Automatically retries the operation
    • B) Handles errors within an async function
    • C) Pauses the function until an error is resolved
    • D) Only logs errors without handling them
    Answer: B. try...catch handles errors within an async function.
  3. In Promise.all, what happens if one of the promises rejects?
    • A) It waits for all promises to complete
    • B) It ignores the rejected promise and resolves with the others
    • C) The entire Promise.all operation rejects
    • D) It retries the rejected promise
    Answer: C. Promise.all rejects if any of the promises in the array reject.
  4. What is the purpose of try...catch in an async function?
    • A) To retry the function if it fails
    • B) To handle errors in synchronous code only
    • C) To catch errors in promises without using .catch()
    • D) To allow synchronous error handling within async code
    Answer: D. try...catch allows synchronous-style error handling within async code.
  5. Which of the following methods returns an array of results with statuses for each promise, regardless of errors?
    • A) Promise.race
    • B) Promise.any
    • C) Promise.allSettled
    • D) Promise.all
    Answer: C. Promise.allSettled returns an array of results with statuses for each promise, regardless of errors.