The Secret Life of JavaScript: The Rejection
Source: Dev.to
Why async errors bypass try/catch
Timothy placed a try/catch block at the top of his application, confident that any error would be caught:
function loadDashboard() {
try {
// Initiating a background network request
fetch('/api/corrupted-data');
console.log("Dashboard loading...");
} catch (error) {
console.error("Safe Landing:", error.message);
}
}
loadDashboard();
The console printed Dashboard loading…. Two seconds later the process crashed with:
UnhandledPromiseRejection: Failed to fetch
What happened?
fetch()returns a pending Promise immediately; it does not wait for the network request.- The
tryblock finishes successfully, so the call stack is unwound and thecatchblock is discarded. - When the Promise later rejects, there is no longer a stack frame for the original
try/catchto handle the error. The rejection becomes an unhandled promise rejection, which modern browsers and Node.js treat as a fatal error.
Attaching the error handler to the Promise
The fix is to attach a handler directly to the Promise:
function loadDashboard() {
fetch('/api/corrupted-data')
.then(data => console.log("Data loaded!"))
.catch(error => console.error("Safe Landing:", error.message));
console.log("Dashboard loading...");
}
- The
catchmethod creates an error boundary on the Promise itself, so when the network request fails, the rejection is routed to that handler instead of floating away.
Using async/await with try/catch
If you prefer the try/catch syntax, await pauses the function execution and preserves the error boundary:
async function loadDashboard() {
try {
console.log("Dashboard loading..."); // moved before the await
// Execution is suspended here; the try/catch stays in memory.
await fetch('/api/corrupted-data');
console.log("Data loaded successfully!");
} catch (error) {
console.error("Safe Landing:", error.message);
}
}
- When
awaitencounters a rejected Promise, the engine resumes the function, restores thetry/catchcontext, and throws the error, which is then caught by the surroundingcatchblock.
Senior Tip: Callbacks and Event Listeners
Callbacks (e.g., setTimeout, DOM event listeners) run in a brand‑new call stack. Wrapping the registration code in a try/catch does not protect the callback body.
// ❌ WRONG: The try/catch runs during setup, not when the event fires.
try {
button.addEventListener('click', () => {
throw new Error("Click failed!"); // crashes the app
});
} catch (e) {
// This block is long gone by the time the click occurs.
}
// ✅ RIGHT: Place the try/catch **inside** the callback.
button.addEventListener('click', () => {
try {
throw new Error("Click failed!"); // caught safely
} catch (e) {
console.error("Safe Landing:", e.message);
}
});