7 reasons why you should use async/await today

Published

For several years node.js programmers have been using callbacks, promises and generators for managing asynchronous functions and honestly that was several years of pain. Every of above approaches had it's own pain points like handling exceptions, chaining, breaking a promise chain etc. But that times are gone. Programmers received async/await and it's highest time to use it for all asynchronous functions. Here is why.

1. Very simple to use

Just prepend your function definition with "async" keyword and voilà.

async function() {
   // voilà!
   if (someCondition) {
      throw new Error('Foo');
   }
   await someAsyncFunction()
}

// manuall conversion to promises...
function() {
   // make sure it always returns a promise (even rejected ones in case of errors)
   if (someCondition) {
      // throw is not allowed
      return Promise.reject(new Error('foo'));
   }
   return someAsyncFunction()
       .then(() => undefined) // in order to achieve exactly the same result as above
}

// extra wrapper which might be a bit problematic for methods definitions
co(function *() {
   if (someCondition) {
      throw new Error('Foo');
   }
   yield someAsyncFunction()
})

// I don't even want to remember how to do it with callbacks...

2. Intuitive and easy to learn

Remember How it feels to learn javascript in 2016 ? Yeah. No more excuses for "mistakes" of the past. No need explain why we use callbacks, promises or generator wrappers. Just add 2 extra keywords.

3. Detecting asynchronous functions upfront

There is no way to reliably detect whether a function is asynchronous without inspecting arguments (very unreliable) or calling it (check whether function returns a Thenable) and you don't want to assume every generator as asynchronous.

In the end. Now you able to very clearly (and easy) express your intentions in the code.

const AsyncConstructor = (() => async function() {})().constructor;

someAsyncFunction instanceof AsyncConstructor

// or
Object.prototype.toString.call(someAsyncFunction) === '[object AsyncFunction]'

// or even easier with predicates@2.0.0
const is = require('predicates');

is.asyncFunction(someAsyncFunction);

NOTE: This doesn't work for transpiled async/await that gets "downgraded" to generators.

4. Error handling

With async/await every exception is always "converted" to rejected promise which wasn't a case for callbacks and promises.

async function foo() {
    // function below does not exist but TypeError will be converted to rejected promise
    someBadReference() 
    await someAsyncFunction()
}

foo().catch(e => console.log('ok')); // works


function foo() {
    // sorry, regular exception will be thrown
    someBadReference(); 
    return someAsyncFunction() // hopefully it will always return a promise as well
}

foo().catch(e => console.log('ok')); // doesn't work

function foo(callback) {
    // same problem as above
    someBadReference(); 
    someCallbackAsyncFunction(callback);
}

foo((e) => console.log('ok')); // doesn't work

co(function *() {
    // works as expected => rejected promise
    someBadReference(); 
    yield someAsyncFunction();
})().catch((e) => console.log('ok')) // works

5. Easy to use everywhere with Typescript and native node support

Since Typescript 2.1 you can use async/await keywords in every environment.

Since version 7.10 Node supports async/await without any special flag. In that case you can switch Typescript compiler target to "es2017" and async/await won't be "downgraded" to generators.

I 99% sure that "babel" is able to achieve the same but not sure how exactly.

6. Easy to implement multiple code paths

Having multiple code paths wasn't easy with callback and promises only. Just look at amount of questions people had about breaking long promises chain and "sick" solutions for that

google -> break promise chain

// this code is bad and rather stupid, but fine for example purposes
async function() {
   try {
       const result = await callSomeRemoteAPI();
   } catch (e) {
       if (e.message.indexOf('not found')) {
           return;
       }
       throw e;
   }

   for (const entry of result) {
       const entryResult = await anotherCallToRemoteAPI(entry);
       if (!entryResult) {
          return;
       }
   }
   return result;
}

I don't even want to remember how achieve the same with callbacks...

7. Safe to use in external libraries

Again, just because async/await might be transpiled to generators then every library consumer might use it without even noticing, since, in the end, async function still returns a simple promise.

Summary

We've been waiting so long for such powerful feature that makes asynchronous programming easy and pleasant. There is no better tool for that today. Just start using it.

Łukasz Kużyński - Wookieb
Thoughts, tips about programming and related subjects to make your job easier and more pleasant so you won't burnout quickly.
Copyright © Łukasz Kużyński - Wookieb 2023 • All rights reserved.