try catch finally Best Practices
Catch only what you handle, use finally for cleanup, avoid empty swallows, and bridge sync try/catch with async/await.
- javascript
- try-catch
try/catch/finally is control flow for exceptions, not a second return channel for every failure. Used well: cleanup is reliable and errors surface with context. Used poorly: empty catches hide production fires.
Shape
try {
risky();
} catch (err) {
// handle or rethrow
throw err;
} finally {
// always runs (unless process dies / infinite loop)
cleanup();
}
finally runs on success, catch, and return from try/catch — it can override return values if it returns itself (avoid returning from finally).
function f() {
try {
return 1;
} finally {
// return 2; // would replace 1 — don't surprise callers
}
}
Catch what you can handle
try {
await save(doc);
} catch (err) {
if (err.code === 'QUOTA_EXCEEDED') {
notify('Storage full');
return;
}
throw err; // unknown → bubble
}
Catch-all that only logs and continues is sometimes OK at a UI boundary; never deep in a library without rethrow.
async/await
async function load() {
try {
const res = await fetch('/api');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
// rejects from await land here
report(err);
throw err;
} finally {
hideSpinner();
}
}
.catch on promises is equivalent for the promise path; try/catch doesn’t catch async errors without await:
try {
fetch('/x'); // returns promise — errors not caught here
} catch (e) { /* no */ }
try {
await fetch('/x');
} catch (e) { /* yes */ }
finally for cleanup
const lock = await mutex.acquire();
try {
await criticalSection();
} finally {
lock.release();
}
Same pattern: close handles, remove listeners, reader.releaseLock(), stop spinners.
Don’t use exceptions for normal branches
// smell
try {
return map[key].value;
} catch {
return defaultValue;
}
// better
return map[key]?.value ?? defaultValue;
Exceptions are for exceptional / non-local failures, not missing keys you expect.
Error objects
throw new Error('meaningful message');
// throw 'string'; // loses stack discipline — avoid
cause: throw new Error('wrap', { cause: err }); // ES2022
Interview answer (out loud)
“I use try/catch for operations that can throw or reject under await, handle only known cases, rethrow the rest, and put cleanup in finally. Empty catch is a last resort at boundaries. try/catch doesn’t intercept promise rejections without await. I don’t use exceptions for expected control flow.”
Promise.finally and try/finally
fetch(url)
.then(parse)
.finally(() => hideSpinner());
// equivalent structure with await
finally on promises doesn’t receive the value/reason; same cleanup mindset.
Catch order with instanceof
try {
await work();
} catch (err) {
if (err instanceof AuthError) redirectLogin();
else if (err instanceof NetworkError) toastOffline();
else throw err;
}
Custom error classes (or err.name) beat string matching messages.
Performance
try blocks aren’t free in ancient engines; modern engines handle them fine. Don’t avoid try/catch for micro-optimization — avoid them for control flow clarity reasons only when a simple check works.
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.