Date
Node JS

Alternate working title: Awaiting for g'Doh!

update 2016-02-03 - slides from a talk based on this: https://chrishiestand.github.io/slides-2016-02-02-js-async-awaitier/

update 2016-01-25 - The first version of this article contained likely misinformation about callbacks and the event loop. It has been corrected and I apologize. I said that the argument to Array.prototype.map() is a callback that gets put on the event loop; that was a misunderstanding and I no longer think it gets put on the event loop. The reason why I think but do not know it's untrue is because it's hard to verify what's on the event loop and I haven't read through to source code of v8 to be sure.

Despite not being a n00b, I made a n00b mistake which only became apparent when using async/await. I was using await yet the code was behaving asynchronously. wtf, mate?

This post explains wtf, mate.

TL;DR

  • an async function is actually returning a promise behind the scenes

Details

See if you can figure out the output from this example:

import _Isofetch from 'isomorphic-fetch';

async function getIp_P() {
    let ip = await fetch('https://api.ipify.org?format=json')
    .then((response) => {
        return response.json();
    });
    return ip;
}

function main() {
    [1,2,3].map(async () => {
        console.log('wan ip:');
        console.log(await getIp_P());
    });
}

main();
Hover for the output
wan ip:
wan ip:
wan ip:
{ ip: '1.2.3.4' }
{ ip: '1.2.3.4' }
{ ip: '1.2.3.4' }
            

If you got it right you get a gold star for the day!

While await within the async function will "pause" execution, we must keep in mind that the async keyword will convert the function to immediately return a promise. This means that after the first lambda function begins awaiting the second lambda function will immediately be called and so forth for each element of the mapped array.

The async keyword signals the interpreter (transpiler in this case) to wrap the function in a promise and return the promise. So you can always await an async function just like you can await a promise - under the hood they are the same thing. This is why console.log(await getIp_P()); works - because getIp_P() actually returns a promise.

Awaiting Callbacks

One way to solve this problem is to use a promise utility to wait until each callback is resolved, in this case bluebird's Promise.map().

import _Isofetch from 'isomorphic-fetch';
import _Promise from 'bluebird';

async function getIp_P() {
    let ip = await fetch('https://api.ipify.org?format=json')
    .then((response) => {
        return response.json();
    });
    return ip;
}

function main() {

    _Promise.map([1,2,3], async () => {
        console.log('wan ip:');
        console.log(await getIp_P());
    }, {concurrency: 1});
}

main();
Hover for the output
wan ip:
{ ip: '1.2.3.4' }
wan ip:
{ ip: '1.2.3.4' }
wan ip:
{ ip: '1.2.3.4' }
            

The Final Test

So if you followed this post you should be able to correctly predict the output of the following code.

import _Isofetch from 'isomorphic-fetch';

async function getIp_P() {
    let ip = await fetch('https://api.ipify.org?format=json')
    .then((response) => {
        return response.json();
    });
    return ip;
}

async function main() {
    let i = 0;

    while( i < 3 ) {
        console.log('wan ip:');
        console.log(await getIp_P());
        i++;
    }
}

main();
Hover for the output
wan ip:
{ ip: '1.2.3.4' }
wan ip:
{ ip: '1.2.3.4' }
wan ip:
{ ip: '1.2.3.4' }
            

Because main() is the only function running in the global context there will be no context switching while we're waiting for getIp_P(). So this version of the code operates sequentially as desired, in all its imperative ugliness.

note: punny alternate title stolen from this ;login: article: https://www.usenix.org/publications/login/dec15/beazley


Comments

comments powered by Disqus