The try/catch/maybe story

#javascript #async-await #promises #try-catch-finally

When we lean about exception handling, and the try/catch/finally statements, we are told that the finally block is guaranteed to always execute. Here is an example:

function doSomething() {

}

try {
  doSomething();
  console.log("Done!");
  return 42;
} catch (e) {
  console.error("Got error:", e);
} finally {
  console.log("Finally!");
}

The code above will successfully execute doSomething (the function doesn’t do anything at the moment), it will print Done, and then, even though there is a return statement, it will print Finally!.

Let’s see what happens if we throw an exception inside doSomething:

function doSomething() {
  throw new Error("Nope!");
}

try {
  doSomething();
  console.log("Done!");
  return 42;
} catch (e) {
  console.error("Got error:", e);
} finally {
  console.log("Finally!");
}

As expected, the exception being thrown will interrupt the execution of the try block, triggering the catch block, which will print Got error: Error: Nope!. After that, the finally block will execute, printing Finally!.


For synchronous code the finally guarantee holds true. The question is, will it also hold true for async code? Technically yes, but with a caveat. The promise that is being awaited needs to be resolved or rejected before the rest of the code can execute.

Let’s return to our code examples and this time use async code.

async function doSomething() {
  return Promise.resolve();
}

try {
  await doSomething();
  console.log("Done!");
  return 42;
} catch (e) {
  console.error("Got error:", e);
} finally {
  console.log("Finally!");
}

Just like in our first example, doSomething will successfully resolve with undefined, after which the rest of the code will execute, printing Done! followed by Finally!.

If we reject from doSomething the result will be similar with the second sync code example:

async function doSomething() {
  return Promise.reject(new Error("Nope"));
}

try {
  await doSomething();
  console.log("Done!");
  return 42;
} catch (e) {
  console.error("Got error:", e);
} finally {
  console.log("Finally!");
}

In both our examples, the promise has settled, which allowed the execution of the rest of the code to continue. However, in the recent months, I have stumbled upon a situation where the promise never settled. We nicknamed that bug the try/catch/maybe bug. Here is how it looks like.

const someCondition = false; 
async function doSomething() {
  return new Promise(resolve => {
    if (someCondition) {
      resolve(); // never resolves because our condition is `false`
    }
  });
}

try {
  await doSomething();
  console.log("Done!");
  return 42;
} catch (e) {
  console.error("Got error:", e);
} finally {
  console.log("Finally!");
}

It’s not rocket science, but it took us a little bit of debugging until we realized that we are not resolving our promises. That’s why I advice to be careful when using conditionals in the new Promise constructor handler.