Monday, October 28, 2019

Published 9:31 PM by with 0 comment

Understanding async/await in JavaScript

Async/await can be confusing, so I figured it was worth writing a full async/await example in JavaScript. This is a simple page that shows usage of it in both client-side JavaScript and Node.js.

Setup

The simple problem here is that we want to send a request from a client to a server, have the server perform a bunch of asynchronous operations, then have the server send a response back to the client when all operations have finished. We want the client to wait for this to finish before alerting the user with how long it took.


Server code

Here is our server code:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const http = require('http');
const port = 8000;
const host = '127.0.0.1';

http.createServer(async function (req, res) {
  let started = new Date();

  let reqs = [];
  for (let i = 0; i < 10; i++) {
      reqs[i] = sleepWrapper(100);
  }
  await Promise.all(reqs);

  let ended = new Date();
  res.writeHead(200, {'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' });
  res.end((ended - started).toString());
}).listen(port, host);

async function sleepWrapper(ms) {
    await sleep(ms);
}

function sleep(ms){
    return new Promise(resolve=>{
        setTimeout(resolve,ms);
    })
}

What does that do? I'm assuming basic familiarity with Node and JavaScript here. Reading through then, on a request it:
  • grabs current time
  • generates 10 'sleepWrapper' calls with 100 as the arg for each
  • awaits all 10 of those
  • grabs current time
  • responds with 2nd time minus the 1st time
What is 'sleepWrapper'? It simply returns a promise that resolves after the specified number of milliseconds.

Thus, this code requests 10, asynchronous, 100 ms waits, 'awaits' all of them, and returns with the time difference between the initial request and the last resolution.

What does that 'await Promise.all(reqs)' line do? It simply blocks code execution in this function until all 'reqs' have resolved. You can see another example here

Since it waits for all to resolve, they all start at almost exactly the same time, and they all requested 100 ms waits, this should respond to the original request with just over 100.


Client code

Here is our client code:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<head>
</head>
<body>
    <button onclick="query()">Test</button>
    <script>
        function sendRequest() {
            return new Promise(function(resolve) {
                let req = new XMLHttpRequest();
                req.onreadystatechange = function() { 
                if (req.readyState == 4 && req.status == 200)
                    resolve(req.responseText);
                }
                req.open("GET", 'http://127.0.0.1:8000/', true);
                req.send(null);
            })
        }

        async function query() {
            let time = await sendRequest().then(function(result) {
                alert(result);
                return result;
            });
            alert(time);
        }
    </script>
</body>

What does this do? When the button is clicked, it calls query which does the following:
  • await a call 'sendRequest'
  • when it finishes (then), pop up an alert with the result, and return the result to the variable 'time'
  • popup an alert with the value for 'time'
The await here simply means 'don't continue this function until sendRequest() resolves'.

Try this code out and confirm that it works.


Changing the code for better understanding

Now...let's get into common pitfalls. What happens if you remove the await in line 19 of the client code? you have 'let time = sendRequest().then...

Try it and see if it makes sense.

What should happen is that instead of getting two alerts with ~100 in them, you get an alert with a promise, and then an alert with ~100. What's happening is that you're setting 'time' equal to the sendRequest() and not waiting for it to resolve, so 'alert(time)' is just showing an unresolved promise. After ~100 ms, '.then(' is called which pops up an alert with the time it took. You have to await server calls if you want the client to stop the calling function while the server does stuff.

For another...what happens if you remove the await on line 22 in the server code? The client is awaiting the server call, the server call generates 10 sleepWrapper requests and awaits them all, and sleepWrapper generates a sleep request and returns immediately (no await). You end up getting two alerts with some very small (probably <10) time. The client code executed in the correct order (alert(result) before alert(time)), but the server was not awaiting correctly.

One more...what happens if you add an await on line 10 in the server code? Now, each req will await before moving on, so the for loop will 'generate req, wait for resolution, generate next req, ...'. Each 'wait for resolution' is ~100 ms, so that loop will take ~1000 ms. It will hit the await Promise.all(reqs) line which will immediately move on because all reqs have already resolved. It will then send back the total time which is ~1000 ms. The server did not execute the requests asynchronously.


Conclusion

This is a really common flow. Instead of generating 10, 100 ms waits, maybe you want to read from 10 files asynchronously. You'll hit it constantly, this can be a pain to debug, so hopefully this example stripped down to the basics makes it a bit clearer.



      edit

0 comments:

Post a Comment