Async/await try-catch blocks and unhandled promise rejections
0 reputation · 16 Jan 2021, 04:25 UTC
0 reputation · 16 Jan 2021, 04:25 UTC
When implementing asynchronous recovery logic in JavaScript, the async/await syntax is typically paired with try...catch...finally blocks to manage failed operations. A critical requirement is ensuring that every asynchronous failure is captured to prevent process termination in environments like Node.js, where unhandled promise rejections are treated as fatal errors.
There is a design tension between handling errors locally within the specific async function to perform immediate recovery and propagating those errors to a global handler for centralized logging. If a promise-returning function is called without the await keyword inside a try block, the rejection bypasses the local catch block entirely.
finally block to execute local cleanup?async/await with legacy .then().catch() chains?35275 reputation · 16 Jan 2021, 13:47 UTC
Propagating errors to a global handler does not interfere with the ability of a finally block to execute. In JavaScript, the finally block is guaranteed to run after the try and catch blocks complete, regardless of whether the operation succeeded or an error was thrown.
If you catch an error locally to perform recovery and then re-throw it to trigger a global logger, the finally block will still execute before the error propagates further up the call stack. This ensures that critical cleanup—such as closing database connections or clearing timers—happens before the process handles the fatal error.
The most common cause of unhandled rejections is calling a promise-returning function without the await keyword inside a try block. Because the function returns a promise immediately, the try block completes synchronously, and any subsequent rejection occurs outside the scope of the catch block.
To ensure rejections are captured when mixing async/await with legacy .then().catch() chains, use these patterns:
await the legacy chain. This forces the promise resolution (or rejection) to occur within the current execution context, allowing the catch block to intercept it.await), attach a .catch() directly to the end of the legacy chain to handle the error locally.async function handleOperation() {
try {
// Pattern: Await the legacy chain to ensure local catch capture
const data = await legacyApiCall().then(res => res.json());
console.log(data);
} catch (error) {
console.error("Local recovery logic:", error);
throw error; // Propagate to global handler
} finally {
console.log("Cleanup executed");
}
}
These behaviors are standard across modern JavaScript engines. In Node.js (v15+), unhandled rejections are treated as fatal errors that terminate the process by default. To verify this behavior, you can implement a function that throws an error inside a try block and observe that the finally block logs its output before the process.on('unhandledRejection') listener or the process exit occurs.
Diagnostic Detail: Are you using a specific framework (like Express or Fastify) that wraps async handlers? Some frameworks automatically catch rejections in route handlers, which may change whether an error reaches a global process handler.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.