Skip to main content

Errors inside functions

Errors inside a function are handled with try...catch, and in asynchronous code with the same try...catch around an await or with .catch() on a promise. The goal of handling is not to hide the failure but to make the function behave predictably: either return a safe value or throw a clear error upwards.

Theory

TL;DR

  • try...catch protects synchronous code: everything in try runs "under protection", and on an error control moves to catch.
  • In async functions the try...catch goes inside, around the await.
  • Without await, promise errors are caught by .catch() in the chain.
  • An error can not only be caught but also rethrown upwards with throw new Error(...).
  • Use try...catch surgically, where you genuinely expect a failure.
  • Do not silence errors without logging, and return a predictable value on failure.

Quick example

javascript
function divide(a, b) { try { if (b === 0) { throw new Error('Division by zero is not possible'); } return a / b; } catch (error) { console.error('Error:', error.message); return null; // safe fallback value } } console.log(divide(10, 0)); // Error: Division by zero is not possible -> null

What happens here:

  • everything inside try runs "under protection";
  • if an error occurs, execution moves into catch;
  • after that the program does not crash, and the function returns a predictable null.

Synchronous error handling

try...catch is used when an error may happen at the moment the function runs: a bad argument, a failed string parse, reading a property of null. The catch block receives an error object that has at least name, message and stack.

It is important to understand the limits: try...catch only catches what is thrown synchronously inside the block. An error from a callback that runs later (in setTimeout, for example) will not reach that catch, because by the time it is thrown the block has long finished.

Asynchronous errors with async/await

For async functions the try...catch goes inside the asynchronous block, around the await. A rejected promise then turns into an ordinary exception that is caught right there:

javascript
async function fetchUser(id) { try { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error('User not found'); return await res.json(); } catch (error) { console.error('Request failed:', error.message); return null; } }

Note the res.ok check: fetch only rejects on a network failure, while a response with status 404 or 500 counts as perfectly successful for it. So an HTTP error has to be thrown by hand.

Errors in promises

If you do not use await, errors are caught with .catch() at the end of the chain. A single .catch() covers every preceding .then():

javascript
fetch('/api/data') .then(res => res.json()) .then(data => console.log(data)) .catch(err => console.error('Error:', err));

A chain without .catch() produces an unhandled rejection, which in Node.js can terminate the process.

Creating and rethrowing errors by hand

Sometimes an error should not only be caught but also passed upwards with extra context. In that case catch throws a new error:

javascript
function parseJSON(str) { try { return JSON.parse(str); } catch (error) { throw new Error('Invalid JSON: ' + error.message); } } try { parseJSON('{invalid}'); } catch (e) { console.error('Caught at the top level:', e.message); }

This turns a low level parsing error into a readable message, while the decision about what to do next belongs to the code that called the function.

Good practices

  • Use try...catch surgically, where you genuinely expect an error, not around the whole function body.
  • Do not silence errors without logging: an empty catch is the fastest way to lose a day of debugging.
  • Return a predictable value on failure: null, {}, false, and document it.
  • For server side functions log through a logger, Sentry or at least console.error.
  • For pure functions prefer throwing over logging: let the calling code decide.

Common mistakes

  • An empty catch. catch (e) {} makes the failure invisible, and a program that "just does nothing" is impossible to diagnose.
  • Wrapping an asynchronous callback in try...catch. Putting setTimeout or an old style callback inside try catches nothing: the error happens after the block has exited.
  • Forgetting await before a promise inside try. Without await the function returns a promise and the rejection flies past the catch.
  • Assuming fetch throws on a 404. It only rejects on a network failure, so the status has to be checked manually.
  • Throwing a string instead of an Error. throw 'oops' deprives you of a stack trace and breaks any code that expects error.message.
  • Catching an error where you can do nothing about it. If a function cannot recover, it is more honest to rethrow than to return a silent null.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.