No More Awaiting for Async Functions
(bouncy music) - Hi.
So, async functions.
Async processing in a single-threaded environment has been a bit of an issue in JavaScript ever since XHR requests were introduced way back in Internet Explorer 5.
In the beginning, we used these kind of event-based callback functions. I actually found this bit of code in a code base that was written this year.
So, I guess somebody out there still likes doing it this way.
I'm pretty sure the rest of us look at this and go, "What the fuck does readyState equals four mean?" So, in this age we're more likely to see these kinds of callbacks.
They're a bit better.
There's no magic numbers, for a start.
They have some obvious error handling.
And they kind of follow a kind of standard. Unfortunately, it doesn't take very many extra requests to turn this guy into callback hell, which naturally brings us to promises.
As I'm sure you're all aware, promises were invented by Barbara Liskov back in 1988.
It took a little while for them to catch on in JavaScript, what with the language needing to be invented and all, but these days there are a shit tonne of libraries available to let you implement promises in your JavaScript application.
And, of course, as of year six, we have native promises.
So, if we were to reimplement our pyramid of doom from the previous slide using promises, it'll look something like this.
This is obviously much nicer.
It's way easier to read.
The error handling follows more closely to the language error handling, with the catch at the bottom there.
And not only does it follow a standard, but the standard is called the A-Plus standard, so it much be pretty good.
But, what happens if don't know in advance which asynchronous calls we need to make, like perhaps we need to pass in a list of things, and make a call for each item on the list one at a time? Or what if we need to need to pull something? So, we need to keep making the same call over and over again until we get a certain response back.
That turns out to be a little bit tricky to do with promises.
So, let's look at an example.
Now, for this example, I know everyone here is probably a web developer, but I'm gonna need you all to pretend that you're robotics programmers, 'cause, you know, you can do anything with JavaScript these days, right? So, you're a robotics developer, you just got a new job, and really want to make a good impression at work, so, you want to get in early.
The things is, you're not really a morning person, so, you do what any developer would do, and you automate the problem away.
You build yourself a series of robots to help you get ready in the morning.
Now, obviously, you need to wait for each robot to finish what it's doing before the next one does its task, so, you implement it with promises.
It probably looks something like this, I don't know, how hard is robotics programming? So, this works really well, right? Your job's going really well.
You've made a good impression.
You don't really feel like you need to impress anyone anymore, and you're kind of sick of getting up early in the morning, so, you decide that what you need is a snooze button. The thing with snooze, of course, is that you don't know in advance how many times you're gonna need to press it on a particular day.
So, you just kind of want that first line of code to keep looping over and over again until you're ready, or you have to get up.
So, let's see how you can do that with promises. This code is kind of complicated, so don't worry, we will go through it in excruciating detail.
The main driver behind it, is this guy here.
This is a generator.
If you're not familiar with generators, they're kind of way to run a function and to pause it halfway through and go off and do something else, and then come back.
Now, like I said, we will go through it.
So, we start off by kicking off the generator. This returns us an iterator object, which is what we use, sorry, we don't kick off the generator, we initialise the generator.
So, we get an iterator object back which gives us a way to communicate with the generator. Then we call next on that iterator.
This kicks off the generator.
So, we set our alarm for 7 o'clock.
The set alarm function is gonna set our alarm, and it's gonna return a promise that will resolve once we hit one of the buttons on the alarm clock.
Now, we're gonna yield that promise back to where the generator was called from. So, when we yield something, we set the value of the iterator to the thing that we're yielding and we set this done property to false.
We're then gonna pass that entire iterator object down into our await function, down in the bottom there.
The await function is gonna check to see if our iterator is done, which it's not, and then it's gonna grab our promise and stick a then on it.
And that's it, it's gonna wait.
So, eventually, our alarm's gonna go off.
We're gonna say, "Screw this, it's 7 o'clock in the morning. "Snooze." And our promise will resolve with a value of true. So, we can straight away then pass that value back into the generator up here, and we're gonna store it in the snooze variable. And then our generator will continue executing. We're gonna go into the while loop here, we're gonna set our alarm again.
This time it's gonna be set for nine minutes in the future, and once again, we're gonna get a promise back that will resolve once we hit one of the buttons on the alarm.
Again, we yield that promise back to where it was called from, down in the bottom there, and then we pass the entire iterator object back into the await function.
The await function checks if it's done, it's not done, so, then we add this then on it.
And at this point, we can just keep looping as long as we want, 'cause every time we set the alarm we get this promise back, and then we stick this then on it, that's gonna mean that, when the promise gets resolved we're just gonna keep the generator off again, which is gonna get us another promise and keep going like that.
At some point, obviously, we are going to have to get out of bed, so we'll hit wake up.
Our promise is gonna resolve to false, which will pass back into the generator once again. And this time it's false, so we're gonna exit the loop, and, again, we make breakfast, we get the promise back, we pass it into our await function, and once we finish breakfast we brush our teeth, because, obviously, that's what you do after you have breakfast, and then we get to the end of our generator. When we get to the end of the generator, we're gonna return.
It's an implicit return, so it's just gonna return undefined, which you can see the value there.
And now done is set to true.
We pass that back into our await function.
Next is done this time, so we return.
Cool.
So, now we have a way of dealing with this problem with promises, but what does any of that have to do with async functions? Well, the answer is very simple.
If we take this generator declaration and turn it into an async function declaration, and then change all of our yield keywords into await keywords, we now have a bonafide async function, and that will work.
There are a couple of things to note.
For a start, we don't need this await function anymore. The JavaScript engine is gonna provide an implementation for us.
And there are a couple of important difference between my implementation and the real thing. For a start, mine fits on a slide, the real one, not so much.
But other than that, if you passed anything that wasn't a promise into my await function, it probably would've caught on fire or something. The real one would deal with it.
You pass it a non-promise value, it'll just go, eh, wrap it up in a promise and send it back to you. And the promise would just resolve to the value passed in. The other thing is error handling.
If any of my promises had been rejected, my await function would have failed silently, which is probably not the behaviour you want. In the real version, if your promise is rejected, it'll throw an exception, and you can just catch it, possibly with that e.
Um, cool.
So, the only other thing, is that now that we've got an async function, we don't need to worry about any of this faffing around with iterators.
If we want to call an async function, we just call it.
When you call it like this, it will return a promise that will resolve once the async function has finished. So, if you need to do anything afterwards you can just stick a then on it.
The value that the promise will resolve with is gonna be the value that you've returned from your function.
And if you opt not to handle exceptions inside your function, then your promise will be rejected, and you can catch it.
Of course, if you are truly committed to the cause, you won't do any of this rubbish, you'll just make your top level function an async function, and you'll await this guy.
Cool.
So, at this point, I'm pretty sure you guys are looking, "Wow. "Async functions look amazing.
"Those devs have thought of everything.
"I can't wait." And you're mostly right, like it's pretty good, but there are a couple of things that we need to watch out for.
First one is a gotcha that's gonna be familiar to anyone who's used generators, and that is you can only use the await keyword immediately inside an asynchronous function. If you put it in a callback function like this, it's not gonna work.
The simplest workaround for this is to use the language level looping rather than the forEach, which is fine for forEach, doesn't work quite so well with things like map and reduce, it's gonna be a bit more complicated to do that. Second thing is not really a gotcha, it's just something that you need to think about when you're writing these functions.
An async function is gonna run through each line synchronously, waiting for each call to resolve before it continues. So, with this function here, we're gonna start on the first line, we're gonna make this asynchronous call, and wait for it to return.
I'm guessing that's gonna take about two seconds. Then we'll go to the next line, and once again, we're gonna make the call, and we're gonna wait for response, and it's gonna take another two seconds.
So, all together, this function is gonna take four seconds.
Instead we can implement it like this.
In this version, we're gonna make the first call and it's gonna go off and do it's thing.
Then immediately we make the second call, and it goes off and does its thing.
And then we wait for the response from the first call, which is gonna take about two seconds, and then we wait for the response from the second call, which has already been waiting for two seconds, so, it's gonna resolve immediately.
So, this guy, is gonna only take two seconds to run. Now, obviously, this isn't going to work in every situation, it's gonna depend on what you're doing.
So, if the second call had relied on the result of the first call, you can't do this.
But it's something to think about.
The last thing that I wanted to talk about was, the example that I gave was a pretty specific use case. It's not something that's gonna come up everyday. But async functions really are intended to be used anywhere that you're using promises at the moment.
So, this is an actual bit of production code, cut down slightly to fit on a slide.
It's making two asynchronous calls.
So, one at the top there, and one down the bottom there, and one just below that.
And it's basically, the first one happens, it waits, the second one happens, if anything goes wrong it catches it, and then at the end, after both calls, it calls something else.
And this is okay, but it's not really obvious that that's what's happening, right? The two calls, they're not even at the same level indentation.
And that return keyword before the second call is vitally important, but it's not obvious that it's important, and if you don't have a really good grasp of how promises work, you might not realise that you need it, and your code will look like it works, but it doesn't, which will be surprising.
So, if we were to reimplement this using async functions, it looks like this.
Now, as you can see, there's the two asynchronous calls right there, and it's really obvious what's going on.
It just looks like synchronous code.
The error handling is all wrapped up in a tri-catch. You know that your StepUpService.go at the end there is gonna get called last, 'cause it's in a finally, and everyone can easily understand what's going on. The other thing I really like about async functions, is writing the unit tests.
So, writing the unit tests for this first version is kind of complicated.
You need test obj that return promises, and those promises need to resolve at exactly the right point in the test, and it gets a bit messy to manage.
The unit test for the second version just looks like this.
The Stub there doesn't even need to return promises because await deals with it all.
And then when you're actually writing your test, you can also make them async functions.
You can async all the things.
Don't forget to call done on the end though, 'cause otherwise you'll be waiting for a long time. Cool.
So, at this point, you're just thinking, "Wow, amazing.
"I really want to use async functions.
"When can I start using them?" And the answer is now.
The current version of all major browsers supports async function out of the box.
IE11 does not support async functions, because IE11 is the reason we can't have nice things. (audience laughs and applauds) If you are transpiling your code, Tracer has support for async functions behind an experimental flag, and Babel has a plugin called Async Functions to Generators, which is part of the ES 2017 preset.
So, cool.
That's pretty much it.
Generators, sorry, async functions aren't doing anything that you can't already do with generators and promises, but they are going to make your code a lot easier to read and a lot easier to reason about, and they are totally available right now.
Thanks.
(audience applauds) (bouncy music)