DEV Community

Benjamin Black
Benjamin Black

Posted on • Updated on

Eliminate try..catch when using async/await by chaining .catch() clauses to async function calls

The syntax for handling errors thrown by async functions is to use try..catch, which can quite often be cumbersome to use, especially when managing variable scope and function logic.

Aside from allowing the use of the await keyword, an async function is simply a function that (always) returns a Promise, so by chaining a call to .catch to the end of a function call to an async function, exceptions can be handled without needing to wrap code with try..catch.

This can be quite useful in many situations.

For example, the LevelDB Get operation throws a NotFoundError error when an item does not exist, when a more useful result is often to return null.

A simple example should demonstrate:

import level from 'level';

const db = level('./test.db');

let value;

try {
  value = await db.get('nonexistent'); // throws NotFoundError
} catch {
  value = null;
}

console.log(value); // 'null'

if (value === null) {
   // not found logic...
}
Enter fullscreen mode Exit fullscreen mode

Instead of using try..catch, we can attach a .catch clause to end of the call to db.get(), like so:

import level from 'level';

const db = level('./test.db');

const value = await db.get('nonexistent').catch(() => null);

console.log(value); // 'null'

if (value === null) {
   // not found logic...
}
Enter fullscreen mode Exit fullscreen mode

The value that is returned by the catch clause will be the resolved value of the entire promise, unless the catch() itself rejects (throws).

That is:

If db.get() resolves, then the value variable gets the resolved value, and the catch clause isn't invoked.

On the other hand, if db.get() rejects (throws), then the catch clause is invoked.

But the catch clause itself always returns a promise. If it returns anything else, the value it returns is implicitly treated like a promise that has resolved to that value.

Because we literally return null in the catch clause, the clause implicitly resolves to null, so the resolved value returned to await is null, and the value variable is thus initialized with null.


In the real world, Get can throw for other reasons, so the catch clause should actually check for that and otherwise rethrow:

const value = await db.get(key).catch((err) => {
  if (err.notFound) {
    return null;
  }
  throw err;
});
Enter fullscreen mode Exit fullscreen mode

Discussion (1)

Collapse
devdufutur profile image
Rudy Nappée • Edited

It seems difficult to read to me...

Why not just keep things simple like that?

try {
  return await findSomething(someUid);
} catch (e) {
  If (e.notFound) {
    return null;
  }
  throw e;
}
Enter fullscreen mode Exit fullscreen mode

You can still have some complex chaining and keep idiomatic async/await structure :

try {
  let res1 = await someAsyncStuff()
  let [res2, res3] = await Promise.all([
    asyncStuff1(res1),
    asyncStuff2(res1)
  ]);
  return await finalCalculation( res1, res2, res3);
} catch (e) {
  return understandWhatFailedWhyAndWhatToDoAfterwards(e);
}
Enter fullscreen mode Exit fullscreen mode