DEV Community

loading...
Cover image for async/await: under the hood

async/await: under the hood

arschles profile image Aaron Schlesinger Originally published at arschles.com Updated on ・4 min read

I'm really interested in concurrency strategies in programming languages, and because there's a lot of written research out there on the topic, you can find lots of strategies out there.

When you look at some of the more modern stuff, you'll find a lot of literature on just about the same pattern: async/await.

async/await is picking up steam in languages because it makes concurrency really easy to see and deal with. Let's look at how it works and why it helps, using Javascript to illustrate the concepts.

I'm a Javascript dabbler at best, but it's a great language to illustrate these concepts with. Don't go too hard on my JS code below 😅

What It's About 🤔

async/await is about writing concurrent code easily, but more importantly, it's about writing the code so it's easy to read.

Solving Concurrency Three Ways 🕒

This pattern relies on a feature called Promises in Javascript, so we're gonna build up from basics to Promises in JS, and cap it off with integrating async/await into Promises.

Promises are called Futures in many other languages/frameworks. Some use both terms! It can be confusing, but the concept is the same. We'll go into details later in this post.

Callbacks 😭

You've probably heard about callbacks in Javascript. If you haven't, they're a programming pattern that lets you schedule work to be done in the future, after something else finishes. Callbacks are also the foundation of what we're talking about here.

The core problem we're solving in this entire article is how to run code after some concurrent work is being done.

The syntax of callbacks is basically passing a function into a function:

function doStuff(callback) {
    // do something
    // now it's done, call the callback
    callback(someStuff)
}

doStuff(function(result) {
    // when doStuff is done doing its thing, it'll pass its result
    // to this function.
    //
    // we don't know when that'll be, just that this function will run.
    //
    // That means that the rest of our ENTIRE PROGRAM needs to go in here
    // (most of the time)
    //
    // Barf, amirite?
    console.log("done with doStuff");
});

// Wait, though... if you put something here ... it'll run right away. It won't wait for doStuff to finish
Enter fullscreen mode Exit fullscreen mode

That last comment in the code is the confusing part. In practice, most apps don't want to continue execution. They want to wait. Callbacks make that difficult to achieve, confusing, and exhausting to write and read 😞.

Promises 🙌

I'll see your callbacks and raise you a Promise! No really, Promises are dressed up callbacks that make things easier to deal with. But you still pass functions to functions and it's still a bit harder than it has to be.

function returnAPromiseYall() {
    // do some stuff!
    return somePromise;
}

// let's call it and get our promise
let myProm = returnAPromiseYall();

// now we have to do some stuff after the promise is ready
myProm.then(function(result) {
    // the result is the variable in the promise that we're waiting for,
    // just like in callback world
    return anotherPromise;
}).then(function(newResult) {
    // We can chain these "then" calls together to build a pipeline of
    // code. So it's a little easier to read, but still. 
    // Passing functions to functions and remembering to write your code inside
    // these "then" calls is sorta tiring
    doMoreStuff(newResult);
});
Enter fullscreen mode Exit fullscreen mode

We got a few small wins:

  • No more intimidating nested callbacks
  • This then function implies a pipeline of code. Syntactically and conceptually, that's easier to deal with

But we still have a few sticky problems:

  • You have to remember to put the rest of your program into a then
  • You're still passing functions to functions. It still gets tiring to read and write that

async/await 🥇

Alrighty, we're here folks! The Promised land 🎉🥳🍤. We can get rid of passing functions to functions, then, and all that forgetting to put the rest of your program into the then.

All with this 🔥 pattern. Check it:

async function doStuff() {
    // just like the last two examples, return a promise
    return myPromise;
}

// now, behold! we can call it with await
let theResult = await doStuff();

// IN A WORLD, WHERE THERE ARE NO PROMISES ...
// ONLY GUARANTEES
//
// In other words, the value is ready right here!
console.log(`the result is ready: ${theResult}`);
Enter fullscreen mode Exit fullscreen mode

Thanks to the await keyword, we can read the code from top to bottom. This gets translated to something or other under the hood, and what exactly it is depends on the language. In JS land, it's essentially Promises most of the time. The results to us programmers is always the same, though:

  • Programmers can read/write code from top to bottom, the way we're used to doing it
  • No passing functions into functions means less }) syntax to forget write
  • The await keyword can be an indicator that doStuff does something "expensive" (like call a REST API)

What about the async keyword⁉

In many languages including JS, you have to mark a function async if it uses await inside of it. There are language-specific reasons to do that, but here are some that you should care about:

  • To tell the caller that there are Promises or awaits happening inside of it
  • To tell the runtime (or compiler in other languages) to do its magic behind the scenes to "make it work"™

🏁

And that's it. I left a lot of implementation details out, but it's really important to remember that this pattern exists more for human reasons rather than technical.

You can do all of this stuff with callbacks, but in almost all cases, async/await is going to make your life easier. Enjoy! 👋

Discussion (14)

pic
Editor guide
Collapse
danstur profile image
danstur

This post falls prey to an issue I find very common to people new to asynchronous code: Conflating asynchronous code with concurrent code. This leads to a lot of pain or at least confusion down the line and it's important to understand the difference.

Concurrency means that two or more threads of execution run concurrently - i.e. in parallel.

Asynchronous code on the other hand is a form of cooperative scheduling, usually implemented via continuation passing style (CPS). Simplified, async code doesn't mean that code runs in parallel just that you switch executing different code on the same thread.

You can combine the two concepts, which is often done, but you don't have to: You can have asynchronous code in a single-threaded program without any problems. Hell you can await a task/promise/whatever you want to call it without ANY thread being involved!

Collapse
michaeltharrington profile image
Michael Tharrington (he/him)

This seems like it's probably helpful advice, but the line "this post falls prey to an issue I find very common to people new to asynchronous code" is condescending and unnecessary. Next time, I'd advise just giving the feedback without categorizing the mistake as something that people new to asynchronous code make.

Collapse
jessekphillips profile image
Jesse Phillips

However your comment falls prey to using concurrent and parallel synonymously where by async/await is in fact concurrent but not parallel.

medium.com/@itIsMadhavan/concurren...

A system is said to be concurrent if it can support two or more actions in progress at the same time. A system is said to be parallel if it can support two or more actions executing simultaneously.

Collapse
danstur profile image
danstur • Edited

@jesse Interesting definition and I can absolutely see the value in distinguishing between the two that way.

That doesn't seem to be the only definition though and not the one I'm used to. If you look at say the C++ memory model definition in the standard or Java Concurrency in Practice (to name one seminal book in that area) both use "concurrently" meaning "parallel".

e.g. from the standard: "Thus a bit-field and an adjacent non-bit-field are in separate memory locations, and therefore can be concurrently updated by two threads of execution without interference"

Thread Thread
jessekphillips profile image
Jesse Phillips

Yeah, I'm not too fond of trying to make a distinction for those terms... I always have to look up which one is which. But asynchronous has the same issue.

Collapse
miketalbot profile image
Mike Talbot

Totally agree, the concurrency is only in the fact that there are multiple "stacks" of things that will happen on continuation. Using async code to perform collaborative tasks is possible but a blunt instrument. I did some stuff around a more fine-grained collaborative multitasking that I talk about here:

Collapse
ben profile image
Ben Halpern

Really well described

Collapse
arschles profile image
Collapse
dansimiyu profile image
dan-simiyu

Simplified and clear explanation. Thanks man

Collapse
triptych profile image
Andrew Wooldridge

Thanks for this!

Collapse
sanchitc99 profile image
Sanchit Chaudhary

Great,looking forward to read your other articles too!

Collapse
timaklyubin profile image
Comment marked as low quality/non-constructive by the community. View Code of Conduct
Просто Тима

Just another clickbait title. Where is the "under the hood" part? It just a really basic tutorial, not at all describing any "under the hood" parts.

Collapse
richytong profile image
Richard Tong

I came searching for the under the hood part too. Here's an article I've found previously. v8.dev/blog/fast-async#await-under...

Collapse
timaklyubin profile image
Просто Тима

Yep... Comment marked as low quality/non-constructive by the community
And publishing clickbait articles is definitely complies with the code of conduct :) I see now