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 singletry...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
- 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. CallloadData
with a callback that handles success and failure cases.
- 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.
- Objective: Practice using
- Using Try-Catch with Async/Await
- Objective: Handle errors in async/await code.
- Instructions: Write an async function
getProfile
that usesawait
to fetch user data. Usetry...catch
to log an error message if data fetching fails.
- 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.
- Objective: Handle errors with
- 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.
- Objective: Use
Multiple-Choice Questions
- 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
.catch()
at the end of the chain. - A) Using a
- 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
try...catch
handles errors within an async function. - 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
Promise.all
rejects if any of the promises in the array reject. - 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
try...catch
allows synchronous-style error handling within async code. - 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
Promise.allSettled
returns an array of results with statuses for each promise, regardless of errors. - A)
