async/await Error Handling in JavaScript: try/catch Patterns That Don't Hide Bugs
async/await makes asynchronous JavaScript read like normal code, and try/catch works with it. But a missing await, an empty catch, or a forgotten rejection can still make errors vanish or crash a Node.js process. These are the patterns that keep failures visible.
Pitfall 1: the empty catch
async function saveProfile(profile) {
try {
await api.put('/profile', profile);
} catch (err) {}
}
async function saveProfile(profile) {
try {
await api.put('/profile', profile);
} catch (err) {
logger.error({ err, userId: profile.id }, 'Saving profile failed');
throw err;
}
}
Pitfall 2: a missing await
try/catch only catches a rejection if the promise is awaited inside the try. Returning or calling a promise without await lets the rejection escape the catch.
try {
return fetchOrder(id); // rejection escapes the catch
} catch (err) { /* never runs */ }
try {
return await fetchOrder(id); // rejection is caught here
} catch (err) { /* runs */ }
Pitfall 3: unhandled promise rejections
Since Node.js 15, an unhandled promise rejection crashes the process by default. Fire-and-forget calls are the usual cause. Either await them, or attach a .catch that logs the error.
// Fire-and-forget, but failures are still logged
sendWelcomeEmail(user).catch((err) =>
logger.error({ err, userId: user.id }, 'Welcome email failed')
);
Running several tasks: all vs allSettled
- Promise.all rejects as soon as one promise rejects. Use it when every result is required.
- Promise.allSettled waits for all of them and tells you which failed. Use it when partial success is fine.
const results = await Promise.allSettled(users.map(notify));
const failed = results.filter((r) => r.status === 'rejected');
if (failed.length) logger.warn({ count: failed.length }, 'Some notifications failed');
Handle errors once, at the boundary
Not every function needs a try/catch. Let errors bubble up to one place that knows what to do: an Express error middleware, a framework error boundary, or the top of a job runner. Catch locally only when you can recover, add context, or clean up (finally is good for cleanup).
Find empty catches and unhandled async code
CodeInspector flags empty catch blocks in JavaScript (JS0001) and TypeScript, and async JavaScript functions with no error handling (JS0005), with the file and line for each. Scan a repository at https://codeinspector.app; new accounts get 100 free credits.