The error that "flies" at another moment
Handling errors in asynchronous code is different because the failure doesn't happen on the line where you wrote the call, but later, when the promise resolves. There are two main tools.
1. try/catch with await
Inside an async function, await turns a rejection into an exception
that you can catch with a normal try...catch, as if it were synchronous code:
async function load() {
try {
const data = await requestData(); // if it rejects, it jumps to the catch
return data;
} catch (error) {
console.log("Failed:", error.message);
return null; // default value
}
}
2. chained .catch()
Without async/await, you use .catch() at the end of the chain:
requestData()
.then((data) => use(data))
.catch((error) => console.log("Failed:", error.message));
The classic mistake: try/catch WITHOUT await
This does NOT work the way you expect:
async function bad() {
try {
requestData(); // the await is missing!
} catch (error) {
console.log("This never runs");
}
}
Without await, the line requestData() only creates the promise and
moves on; the try already finished when, much later, the promise rejects. The
synchronous catch can't catch a failure that happens outside its lifetime.
It's like closing the safety net before the trapeze artist jumps.
Rule: a
try/catchonly catches a promise's rejection if you putawaitin front of it (or if you return the promise and handle the error with.catch).
Uncaught rejections
If a promise rejects and no one handles it (neither await in a try, nor a
.catch), an unhandled rejection occurs. In the browser you'll see a
warning in the console; in Node, by default, it terminates the process. That's
why every promise that can fail should have its error handler.
// ❌ Unhandled rejection: warning / crashed process
Promise.reject(new Error("no one catches me"));
// ✅ Handled
Promise.reject(new Error("I'm caught now")).catch((e) => console.log(e.message));
Examples
try/catch with await returns a default value
function fail() {
return Promise.reject(new Error("boom"));
}
async function safe() {
try {
return await fail();
} catch (e) {
return "default value";
}
}
safe().then((v) => console.log(v)); // "default value"
Without await, the catch does NOT catch the rejection
function fail() {
return Promise.reject(new Error("boom"));
}
async function bad() {
try {
const p = fail(); // without await
p.catch(() => {}); // we silence it so the demo doesn't break
return "the try finished without catching anything";
} catch (e) {
return "this does NOT run";
}
}
bad().then((v) => console.log(v));