Recent chats
ASKGEEZ Q&A GUIDETechnology & devices

How should I catch errors from an async function called by another function?

Quick answer

If an async function awaits another async function, put try/catch around the await when that function can make a useful local decision about the failure. If it cannot resolve the problem, let the rejection propagate so a caller can handle it. Catch optional work locally when you can safely continue; do not turn a failed critical operation into success by returning a misleading fallback. [S1]

LET’S WORK THROUGH IT

The question, the details, the next step.

Question

I have one async function calling another with await. Where should I put the try/catch?

Geez

Put it at the layer that can make a meaningful decision. The function doing the work can catch an error if it can recover, skip an optional step, or add useful context. Otherwise, allow the rejection to travel back to its caller, and catch it there if that caller can respond appropriately. An await inside a try lets that catch handle a rejected promise; a catch in a narrower scope can handle a failure locally, while an uncaught rejection can continue to the caller. [S1]

For example, suppose loadRecord is essential to buildPage, while loadRecommendations is optional. The example below assumes those functions return promises and that an empty recommendation list is an acceptable fallback:

Follow-up question

Can you show the difference between catching an optional failure locally and letting an important failure reach the caller?

Geez

Here is a nested example. The local catch only covers the optional recommendations request; the required record request is outside it, so its failure can reject buildPage for the caller to handle.

async function buildPage(id) {
  const record = await loadRecord(id);

  let recommendations = [];
  try {
    recommendations = await loadRecommendations(id);
  } catch (error) {
    // Assumption: recommendations are optional, so an empty list is acceptable.
  }

  return { record, recommendations };
}

async function showPage(id) {
  try {
    const page = await buildPage(id);
    render(page);
  } catch (error) {
    showPageError(error);
  }
}

Here, a failed loadRecord rejects buildPage; showPage can catch that rejection. A failed loadRecommendations is handled inside buildPage, which deliberately continues with the assumed fallback. That fallback is suitable only if the rest of your program can correctly treat missing recommendations as acceptable. [S1]

Follow-up question

What if the function in the middle cannot recover or show an error itself? Should I catch and rethrow?

Geez

Usually, it can simply leave the rejection uncaught and let its caller decide. A catch that only rethrows adds no handling by itself. Catch and throw again when you have a specific reason to change the error, such as adding context that helps the next layer; avoid replacing it with a vague success value.

For instance, a middle function might do const record = await loadRecord(id); return format(record);. If loadRecord rejects, that rejection can reach the caller. If the middle function catches the failure and returns { record: null }, it has changed failure into a value. Only do that when null is an explicitly valid result and callers are designed to handle it.

Follow-up question

How can I check that my catch is handling the right failure and not hiding a bug?

Geez

Trace each awaited operation and ask what the program should do if it rejects. Keep a local catch narrow when only one step is optional; a broad catch around several awaits can make it harder to tell which operation failed or can accidentally treat a required failure as optional. At the outer layer, choose a response the application actually supports, such as displaying an error state in this illustrative example.

A practical check is to imagine two cases separately: the required request fails, and the optional request fails. The required failure should still reach a layer that can respond to it; the optional failure should continue only if its fallback is genuinely safe for your application. Promise behavior does not determine which operations are optional or what fallback your particular application should use. [S1]

A practical checklist

  • Mark each awaited operation as required or optional for this specific flow.

  • Place a local try/catch only where the function can make a sound recovery decision.

  • Let failures that the current function cannot resolve propagate to a caller that can respond.

  • Use a fallback only when it is an explicitly acceptable result and downstream code supports it.

  • Walk through one required-operation failure and one optional-operation failure to verify both paths.

A few more useful answers

Does `try`/`catch` work around an `await`?

Yes. If the awaited promise rejects, a surrounding try/catch can handle that failure. The catch handles only failures that reach its scope; a narrower catch may handle a particular step locally. [S1]

Should every async function catch its own errors?

No. Catch inside a function when it can recover or intentionally choose a valid fallback. Otherwise, let the rejection propagate to a caller that can decide what to do. [S1]

Is returning an empty value from `catch` the same as propagating an error?

No. Returning a fallback handles the failure by producing a value, so callers may no longer see a rejection. Use that approach only when the fallback is valid for the application.

Sources

References used for this guide.

  1. [S1] Using promises — JavaScript | MDN
    developer.mozilla.org · Checked

YOUR QUESTION. YOUR DETAILS.

Let’s work through your situation.

Get a tailored answer or help with your next task. Starts at $1, with a quote before you pay.

Ask your own question
← Back to Geez at Work