BattlefyBlogHistoryOpen menu
Close menuHistory

Why we moved to await/async from promises as soon as we can

Feng Wu September 28th 2017

Node 8 Long term support version is about to be released this October. I can’t wait to start using it in our backend microservices. The feature I am most excited for is await/async.

At Battlefy we’ve been using the await/async feature in our frontend development for a while. Our team really appreciates the readability improvement it brought. This is especially useful in cases where the resolved promise is used in more than one place:

return db.findOne('foo')  
  .then((foo) => db.findOne('bar'))  
  .then((bar) => /*I can't access foo here*/)

//to be able to access foo, I have to resolve the second promise while resolving the first promise
return db.findOne('foo')  
  .then((foo) => db.findOne('bar')  
    .then((bar) => together(foo, bar);  
);

//an alternative to avoid the promise hell
const fooPromise = db.findOne('foo');  
const barPromise = fooPromise
  .then((foo) => db.findOne('bar');  
return Promise.all([
  fooPromise,
  barPromise
])  
  .then([foo, bar]) => together(foo, bar));

It’s much cleaner with await:

const foo = await db.findOne('foo');  
const bar = await db.findOne('bar');  
return together(foo, bar);

Aside from readability improvement, the other big benefit of await/async is it upholds the promise returning function’s contract:

function foo(input) {  
  if (input === 'can be done synchronously') {  
    return 42;  
  } else {  
    return db.findOne('foo');  
  }  
}

The client of this function must account for the two code paths that one is returning an integer, and the other is returning a promise. The better API this function can provide is:

function foo(input) {  
  if (input === 'can be done synchronously') {  
    return Promise.resolve(42);  
  } else {  
    return db.findOne('foo');  
  }  
}

This is simple enough. But sometimes when we refactor old code we might overlook code paths that are rarely used, which breaks the function’s contract under certain circumstances. This will never happen if we convert the function to an async function, as async functions will always return promises.

async function foo(input) {  
  if (input === 'can be done synchronously') {  
    return 42; //42 will be automatically wrapped in a promise  
  } else {  
    return db.findOne('foo');  
  }  
}

The final thing I’d like to talk about in this post is error handling. Consider the following code:

function bar(input) {  
  if (input === bad) {  
    throw new Error('bad input');  
  }   
  return db.findOne('good code path');  
}

The API of this function is that it might return a resolved or rejected promise, or it might throw. To use it properly:

try {  
  bar()
    .then(handleResolvedPromise)
    .catch(handleRejectedPromise)  
} catch (e) {  
  //catch the thrown bar() error here  
}

//alternatively, if we wrap it this way, we can convert the thrown error into a rejected promise:
Promise.resolve()  
  .then(() => bar())  
  .then(handleResolvedPromise)  
  .catch(handleRejectedPromiseAndBadInputError)

The error handling in this case seems laborious. To make it easier we can change the function’s API to not throw, only returning rejected promises:

function bar(input) {  
  if (input === bad) {  
    return Promise.reject(new Error('bad input'));  
  }   
  return db.findOne('good code path');  
}

//usage can be  
bar()
  .then(handleResolvedPromise)
  .catch(handleBadInput)

But we can’t always guarantee that a promise returning function won’t throw errors. Error throwing might happen in 3rd party libraries:

function bar(input) {  
  thirdPartyCalculation(input); //may throw error  
  return db.findOne('good code path'); //may reject promise  
}

//safer version:  
function bar(input) {  
  try {  
    thirdPartyCalculation(input); //may throw error  
  } catch (e) {  
    return Promise.reject(e);  
  }  
  return db.findOne('good code path'); //may reject promise  
}

That’s a lot of boilerplate code! Well, when you convert your functions to async functions, errors to be thrown in those functions will be automagically wrapped into a rejected promise:

async function bar(input) {  
  if (input === bad) {  
    throw new Error('bad input');   
    //this will be wrapped in a rejected promise.  
  }   
  return db.findOne('good code path');  
}

//usage can be  
bar()
  .then(handleResolvedPromise)
  .catch(handleBadInput);
//or  
try {  
  const foo = await bar();  
} catch (e) {  
  handlesRejections();  
}

Now we will never have to worry a promise returning async function throws!

Further reading: https://medium.com/@pyrolistical/how-to-get-out-of-promise-hell-8c20e0ab0513

2023

2022

Powered by
BATTLEFY