Errors in async functions
Errors inside async functions are automatically turned into a rejected promise (Promise.reject). That is why you handle them either with a try...catch block inside the function or with a .catch() method on the promise the function returns.
Theory
TL;DR
- An
asyncfunction always returns a promise, and athrowinside it becomes a rejection. awaiton a rejected promise throws an exception, so an ordinarytry...catchcatches it..catch()on the function call is the second, equally valid option, convenient at the edge of the application.- One
trycan cover severalawaitcalls, or eachawaitcan get its owntryfor a targeted reaction. - A
returnfrom thecatchblock heals the situation: the function's promise fulfils with a default value. - In
Promise.allone failing promise brings the whole call down, so for partial results you usePromise.allSettled.
Quick example
async function getData() {
try {
const res = await fetch("https://no-such-domain.example/data");
const data = await res.json();
console.log(data);
} catch (error) {
console.error("An error occurred:", error.message);
}
}
getData();
// Output: An error occurred: fetch failedHere try...catch works exactly as in synchronous code, but it catches errors from every await inside the block.
Why this works
await pauses the function until the promise settles. If the promise is rejected, await raises an exception and control moves to catch. The code after a failed await does not run at all.
async function example() {
const result = await Promise.reject("Failure!");
console.log(result); // will not run
}
example().catch(console.error);
// Output: Failure!That is the whole trick of async/await: a rejected promise becomes a regular exception, and a regular exception becomes a rejected promise again at the function boundary.
A shared try block and local control
Several await calls can be wrapped in a single try if all the errors are handled the same way:
async function loadUserData() {
try {
const user = await fetch("/api/user");
const posts = await fetch("/api/posts");
const comments = await fetch("/api/comments");
} catch (e) {
console.error("Failed to load the data:", e);
}
}If any of the operations fails, control moves to catch immediately, and the remaining requests never even start.
Sometimes it is better to wrap individual await calls, when the reactions must differ:
async function example() {
try {
const user = await fetchUser();
} catch (e) {
console.error("Failed to load the user:", e);
}
try {
const posts = await fetchPosts();
} catch (e) {
console.error("Failed to load the posts:", e);
}
}Without try...catch the promise simply rejects
If you do not wrap await in try...catch, the function just returns a rejected promise, which you handle with .catch() at the call site:
async function fail() {
throw new Error("Something went wrong");
}
fail().catch(err => console.error("Caught:", err.message));
// Output: Caught: Something went wrongThis is fully equivalent to .catch() handling on ordinary promises.
The same .catch() can be attached to a single await when you only need to suppress the error and continue with a safe value:
async function getUser() {
const res = await fetch("/api/user").catch(err => {
console.error("Request failed:", err);
return null; // return a safe value
});
console.log("Result:", res);
}
getUser();And if you return a value from the catch block, the function fulfils successfully and that value reaches .then():
async function example() {
try {
await Promise.reject("Failure!");
} catch (e) {
console.log("Caught:", e);
return "Default value";
}
}
example().then(result => console.log("Result:", result));
// Output:
// Caught: Failure!
// Result: Default valueErrors in parallel await calls
If you run several operations in parallel through Promise.all and at least one promise rejects, the whole Promise.all fails with the first error:
async function parallel() {
try {
const [a, b] = await Promise.all([
Promise.resolve("ok"),
Promise.reject("failure"),
]);
} catch (e) {
console.log("Caught:", e);
}
}
parallel();
// Output: Caught: failureIf you need the results of every operation regardless of which ones failed, use Promise.allSettled: it always fulfils and returns an array of objects whose status is either "fulfilled" or "rejected".
Best practices
| Situation | What to use |
|---|---|
| Handling errors inside one function | try...catch |
| Handling errors when calling an async function | .catch() |
| Different error types with different reactions | Several separate try...catch blocks |
Several parallel await calls | Promise.allSettled() instead of Promise.all |
| Logging and rethrowing | catch (e) { log(e); throw e; } |
Common mistakes
- Forgetting
awaitbefore the call. The function returns a promise instead of a value, and the error never reaches thetry...catch, which has already finished. return promiseinstead ofreturn await promiseinsidetry. Withoutawaitthe rejection happens after control has lefttry, so the localcatchnever sees it.- Calling an async function with no handling at all. That gives an
unhandledrejectionin the browser and a terminated process in Node.js 15 and newer. - Assuming
fetchthrows on 404 or 500.fetchrejects only on a network failure, so the status has to be checked manually throughres.ok. - Catching an error and swallowing it silently. An empty
catchturns a failure into a success withundefined; either log it andthrow e, or return an explicit default value. - Sequential
awaitcalls where parallel ones are needed. Three independent requests in threeawaitstatements run one after another; start them throughPromise.allorPromise.allSettled. - A
try...catcharound asetTimeoutcallback. An exception from a timer callback goes to the global handler, not to yourcatch; the asynchronous operation has to be wrapped in a promise.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.