ESC

Type to search the knowledge base.

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.

beginner3 min read
  • 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 guides