How to write async await without try-catch blocks in Javascript

ES7 Async/await allows us as developers to write asynchronous JS code that look synchronous. In current JS version we we're introduced to Promises, that allows us to simplify our Async flow and avoid Callback-hell.

A callback-hell is a term used to describe the following situation in JS:

function AsyncTask() {  
   asyncFuncA(function(err, resultA){
      if(err) return cb(err);

      asyncFuncB(function(err, resultB){
         if(err) return cb(err);

          asyncFuncC(function(err, resultC){
               if(err) return cb(err);

               // And so it goes....
          });
      });
   });
}

This creates a hard to maintain code, and makes control flow a really hard task. Just think about an if statement that need to execute other Async method if some result from callbackA equals to 'foo'.

Promises to the rescue

With promises and ES6 on board, we can simplify our previous code nightmare to something like this:

function asyncTask(cb) {

   asyncFuncA.then(AsyncFuncB)
      .then(AsyncFuncC)
      .then(AsyncFuncD)
      .then(data => cb(null, data)
      .catch(err => cb(err));
}

Looks much nicer don't you think ?

But in real world scenarios the async flow might get a little more complex, for instance in your server model (nodejs) you might want to save an entity to a database, then look for some other entity based on saved value, if that value exists do some other async task, after all tasks finished you might want to respond to the user with the created object from step 1. And if error occurred during one of the steps you want to inform the user with the exact error.

With promises of-course it would look much cleaner then with plain callbacks, but still, it can get a little messy IMHO.

ES7 Async/await

Note: You will need to use a transpiler in order to enjoy async/await, you can use either babel or typescript to the polyfills required.

That's where I find async await really useful, it allows you to write code like this:

async function asyncTask(cb) {  
    const user = await UserModel.findById(1);
    if(!user) return cb('No user found');

    const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});

    if(user.notificationsEnabled) {
         await NotificationService.sendNotification(user.id, 'Task Created');  
    }

    if(savedTask.assignedUser.id !== user.id) {
        await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
    }

    cb(null, savedTask);
}

The code above looks much cleaner, but, what about error handling?

When making async calls something may happen during the execution of the promise(DB connection error, db model validation error, etc..)

Since async functions are waiting for Promises, when a promise encounters an error it throws an exception that will be catched inside a catch method on the promise.

In async/await functions it is common to use try/catch blocks to catch such errors.

I'm not coming from a typed language background, so the try/catch adds for me additional code that in my opinion doesnt look that clean. I'm sure it's a matter of personal preference, but that's my opinion.

So the previous code will look something like this:

async function asyncTask(cb) {  
    try {
       const user = await UserModel.findById(1);
       if(!user) return cb('No user found');
    } catch(e) {
        cb('Unexpected error occurred');
    }

    try {
       const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
    } catch(e) {
        cb('Error occurred while saving task');
    }

    if(user.notificationsEnabled) {
        try {
            await NotificationService.sendNotification(user.id, 'Task Created');  
        } catch(e) {
            cb('Error while sending notification');
        }
    }

    if(savedTask.assignedUser.id !== user.id) {
        try {
            await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
        } catch(e) {
            cb('Error while sending notification');
        }
    }

    cb(null, savedTask);
}

A different way of doing things

Recently I've been coding with go-lang and really liked their solution that looks something like this:

data, err := db.Query("SELECT ...")  
if err != nil { return err }  

I thinks it's cleaner then using the try-catch block and clusters the code less, which makes it readable and maintainable.

But the problem with await is that it will silently exit your function if no try-catch block was provided for it. And you won't have a way to control it unless providing the catch clause.

When me and Tomer Barnea a good friend of mine sat and tried to find a cleaner solution we finished using the next approach:

Remember that await is waiting on a promise to resolve ?

With that knowledge we can make small utility function to help us catch those errors:

// to.js
export default function to(promise) {  
   return promise.then(data => {
      return [null, data];
   })
   .catch(err => [err]);
}

The utility function receives a promise, and then resolve the success response to an array with the return data as second item. And the Error received from the catch as the first.

And then we can make our async code to look like this:

import to from './to.js';

async function asyncTask(cb) {  
     let err, user, savedTask;

     [err, user] = await to(UserModel.findById(1));
     if(!user) return cb('No user found');

     [err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
     if(err) return cb('Error occurred while saving task');

    if(user.notificationsEnabled) {
       const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));  
       if(err) return cb('Error while sending notification');
    }

    cb(null, savedTask);
}

The example above is just a simple use-case for the solution, you can attach interceptor inside the to.js method which will receive the raw error object, log it or do whatever you need to do with it before passing it back.

There is a simple NPM package we created for this library, you can install it using:
Github Repo

npm i await-to-js  

This post is just a different way of looking on the async/await feature, it's totally based on personal opinion. You can achieve similar results using promises, single try-catch, and many other solutions. As long as you like something and it works for you stick with it :)

Dima Grossman

Fullstack web developer and JavaScript enthusiast.

Tel Aviv

Subscribe to Dima's code blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!
comments powered by Disqus