Wait For It…
Define Async/Await in JavaScript in One Sentence
The async
/await
pattern allows non-blocking code to be written as though it’s blocking code.
Rewind
Languages without multi-threading may wish to run things in the background. One way to do that is via callbacks. The best example is the classic setTimeout
callback in the browser:
setTimeout(function(){ alert("Hello"); }, 3000);
Here we offer a callback – the anonymous function
– to something which will, in the background, do its own thing (waiting for a timeout in this case) and then, when there’s available CPU for processing it, execute our callback.
The problem with using callbacks is that it’s quite a backwards way of programming. We have to construct the callback at the point of invoking the background job, and then our main activity either goes onto something else, or then ends.
If there are a chain of activities to perform, then the callback we pass must manage that chain, and it’s not very easy to read such code.
Promise You’ll Do Better
Enter Promise
– the JavaScript wrapper for callbacks.
A Promise
wraps some activity that will eventually complete. It may have already completed it. Promises can be composed with additional follow on behaviour via .then
. If there’s an error running a promise, then the .catch
behaviour can be used to handle that error.
Let’s look at the timeout as a promise:
new Promise((resolve) => setTimeout(resolve, 3000)) .then(() => alert("Hello"));
The first line constructs a promise that will be resolved when the timeout has been executed. The constructor of the promise invokes the function we’re passing in, giving it the resolve
callback. The .then
here is invoked on the successful completion of the timeout.
Which means we can also add more behaviour to the promise if we wish. Rather nicely, the then
function can also accept a promise.
new Promise((resolve) => setTimeout(resolve, 3000)) .then(() => alert("Hello")) .then(() => new Promise((resolve) => setTimeout(resolve, 5000)) .then(() => alert("Hello again"));
Note, in the above, none of the background tasks return a value, so we’re not passing those values down to the .then
. If they did, then it might look like this:
new Promise((resolve) => readUser(id, (user) => resolve(user)) .then((user) => readUserProfile(user, (profile) => alert(profile));
And if we were working with nice functions that didn’t use callbacks, but natively returned promises, it might even be like this:
readUser(id) .then((user) => readUserProfile(user)) .then((profile) => findUserExpiry(profile)) .then((expiry) => alert(expiry));
And let’s say it’s easy to add error handling with .catch
and that any throwing within the process results in the error route being taken.
We can see that promises are a neater mechanism than callbacks.
Wait One Minute
Ask yourself this. If you were writing blocking code, where you can use the promises mechanism above if you wanted to… would you?
Is the above an excellent way to write code? or is it an excellent workaround for background code where we stop having control in this function the moment we invoke that background code?
Don’t get me wrong, the transformation code above is very convenient. Consider mapping and filtering a list:
const processed = ['A', 'B', 'C', 'D', 'E'] .map(value => value.toLowerCase()) .filter(value => !ignores[value]) .reduce((a, b) => a + b, '');
This feels like the same basic functional programming as all the .then
calls doesn’t it?
Except… the flow of control of the program goes to the next line of our function when this is calculated and we can USE the value constructed.
This fluent style of coding is great for making transformations. However, in practice, it’s not as easy to debug as if each line were laid out one after the other in a non-functional style.
Enter Async/Await
An async
function is technically a glorified promise. If you call it, it runs in the background. It returns a Promise and you can mix them with the Promises API.
Whatever you do, don’t cross the streams!!!
You can mix Promises and async
/await
, but really really really try not to.
The effect of this pattern is generally that all the non-blocking code ends up in Promise
returning functions tagged with async
. Anything which wants to process the result of non-blocking code, joins the async
crew. Anything which wants to fire and forget the non-blocking code doesn’t have to.
Genuinely non-blocking code is unaffected by this.
The result, though is that our database example earlier becomes:
// before readUser(id) .then((user) => readUserProfile(user)) .then((profile) => findUserExpiry(profile)) .then((expiry) => alert(expiry)); // within an async function const user = await readUser(id); const profile = await readUserProfile(user); const expiry = await findUserExpiry(profile); alert(expiry);
This is, in my view, massively simpler than the functional approach. It can be debugged by stepping over each line.
Error Handling
Any error handling in async
/await
is simply enclosed in try
/catch
blocks as per blocking code. If catch
blocks hit blocking or non-blocking errors as they try to perform alternative paths, even that’s relatively straightforward.
Summary
There’s a natural evolution in languages from callbacks, through to promises, through to pseudo blocking code. More than JavaScript has made this exact series of steps.
The last stage is the pseudo-blocking code which is easier to reason about, and seems to flatten out the hierarchy of calls that are made some time in the future.
There’s also a great article on this on Hackernoon, which I always refer people to.
Published on Java Code Geeks with permission by Ashley Frieze, partner at our JCG program. See the original article here: Wait For It… Opinions expressed by Java Code Geeks contributors are their own. |