45

I'm having a hard time wrapping my head around the use of async/await and regular sync function calls in JavaScript.

Let's say I have two functions:

Function 1:

async function doSomething() {
    const result = await doExpensiveOperation()
    const result2 = await doAnotherExpensiveOperation()
    return { result, result2 }
}

Function 2:

function doSomething() {
    const result = doExpensiveOperation()
    const result2 = doAnotherExpensiveOperation()
    return { result, result2 }
}

Based on my understanding, these two functions seem equivalent. In Function 1, the first operation is executed and the program needs to WAIT for the results of that operation before executing the next line and then needs to WAIT for the results of that before executing the return statement. How is that any different than Function 2 which executes its statements synchronously?

I think the intention is that Function 1 supposedly unblocks JavaScript's thread of execution and allows it to execute statements past the await but that doesn't seem to fulfill the definition of "await" which sounds to me like it needs to "wait" for the results of something.

Please help get me unstuck on this basic concept.

10
  • 15
    This answer is for C#, but mostly applicable to Javascript's flavor of async/await and the analogy used is golden: stackoverflow.com/a/34681101/3757232 Nov 19, 2021 at 13:11
  • 3
    @JaredSmith: Note that JavaScript is a single-threaded language while C# is a multi-threaded language, so the purpose behind await is quite different despite the similar-looking syntax. In a multi-threaded language, a blocking operation does not make the application unresponsive or inefficient, since other threads will continue running and use the available cycles. Non-blocking operations are preferred to avoid thread starvation, where all available threads are blocked by IO operations, so the OS needs to create new threads.
    – JacquesB
    Nov 19, 2021 at 17:10
  • @JacquesB I'm aware. The kitchen analogy still holds in both cases: from the application developer's perspective it's not usually (at least in my problem domain) super-relevant whether a given async construct is multiplexed to real threads (a la golang) or not. Nov 19, 2021 at 17:54
  • 3
    Waiting for the result of a sync call is like being put on hold on the telephone until the result comes. Waiting for the result of an async call is like leaving a message with your phone number to call back with the result. The point is that while you have to wait for the result in both cases, in the latter case you can do other stuff while you're waiting.
    – BobRodes
    Dec 21, 2021 at 0:21
  • 1

6 Answers 6

69

You are correct that await “blocks” the current task. But there might be more than one task awaiting execution at the same time. While one task is awaiting some result, another task can run.

For example, let's assume I need to doSomething(1) and doSomething(2). Without async/await they'd execute one after another, leading to long latency. But with async, or equivalently with callbacks, the execution can be interleaved. Here's an illustration with ASCII-art:

  • Sequential execution – time spent waiting is wasted.
                    ┌───┬──────┬───┐
    doSomething(1)  │   │ WAIT │   │
                    └───┴──────┴───┘
                                   ┌───┬──────┬───┐
    doSomething(2)                 │   │ WAIT │   │
                                   └───┴──────┴───┘
                    ├───────────────────────────────▶ time
    
  • Concurrent execution – time spent waiting can be used to execute another task.
                    ┌───┬──────┬───┐
    doSomething(1)  │   │ WAIT │   │
                    └───┴──────┴───┘
                        ┌───┬──────┬───┐
    doSomething(2)      │   │ WAIT │   │
                        └───┴──────┴───┘
                    ├───────────────────────────────▶ time
    

A task might be waiting for a variety of reasons, for example:

  • waiting for user interaction, e.g. that the user clicks a button or grants a permission
  • waiting for a HTTP response or other network interaction
  • waiting for a timer to run out

With Promises, every async/await code can be equivalently written with callbacks. It's just that callbacks are typically less convenient. Here's your original code:

async function doSomething() {
    const result = await doExpensiveOperation()
    const result2 = await doAnotherExpensiveOperation()
    return { result, result2 }
}

And here's the code translated to use promise callbacks:

function doSomething() {
    return doExpensiveOperation().then(result => {
      return doAnotherExpensiveOperation().then(result2 => {
        return { result, result2 }
      })
    })
}

Note that each await introduces a clear ordering. Stuff before the await must complete before the await continues. If you'd want the two expensive tasks to have the chance to execute concurrently, it would rather be written like this:

async function doSomething() {
    // spawn the tasks
    const task1 = doExpensiveOperation()
    const task2 = doAnotherExpensiveOperation()

    // wait for both tasks to complete
    const [result, result2] = await Promise.all([task1, task2])
    return { result, result2 }
}
8
  • The final example can be return { await task1, await task2 }, leaving the awaits to the last possible moment
    – Caleth
    Nov 19, 2021 at 9:12
  • 7
    @Caleth I don't think that's legal syntax in an object literal ({a} is a shorthand for {a: a}, and {await a} is a syntax error). But I updated the last example to use Promise.all() to clarify that I don't care which task completes first.
    – amon
    Nov 19, 2021 at 10:04
  • 3
    I might suggest moving the Promise.all example to the top - it's a more direct answer to "what's the difference?". That, and I'd personally include a sentence along the lines of "You await something that has work OTHER than running more code so that other code may run", but that's up to you. Nov 19, 2021 at 13:59
  • 6
    @JacquesB I think "without async/await" refers to the doSomething itself not using await (like in the OPs question) and being synchronous (not asynchronous-sequential). But yeah, this could need some clarification, it's not even clear that in the OP's code the called functions also would be synchronous.
    – Bergi
    Nov 19, 2021 at 22:19
  • 1
    @Caleth No, it cannot. Even fixing the syntax error in the object literal, doing await task1; await task2; is just wrong, you must use Promise.all for correct error handling.
    – Bergi
    Nov 19, 2021 at 22:22
12

I think the intention is that Function 1 supposedly unblocks JavaScript's thread of execution and allows it to execute statements past the await

Not exactly. It allows JavaScript to execute separate statements triggered by other events. So if your doExpensiveOperation is a long-running API query, the user can continue clicking on buttons and whatnot and JavaScript will be able to run those event handlers. With a synchronous wait, it will look to the user like the page "hangs" while you wait for the network response.

It could also be processing the results from a previous network request or an animation from a timer or something. In other words, you could have multiple doSomething running concurrently, even though they must take turns running one at a time.

11

Based on my understanding, these two functions seem equivalent.

They do behave similar. But the difference is subtle.

One selling point of NodeJS (in the beginning) is (was) the possibility of having non-blocking operations. Put in simple terms:

Call a function and deal with the results later.

That is what async means at its core: Start doing stuff now and deal with the results later.

Basically under the hood, it all comes down to callbacks. A callback is a function, which is called from within another function. It is just another function passed as a parameter:

const fn = (callback)=>callback();
fn(()=>console.log("Bananas!"))

That's it. No more magic involved.

If fn is an asynchronous call, the callback is called later in time when the asynchronous call is finished (successfully or with errors).

Back in the day, when we had e.g. jQuery.ajax() to do things like fetching data from a server and doing things, when everything went well / bad

$.getJSON( "ajax/test.json", function( data ) {
  var items = [];
  $.each( data, function( key, val ) {
    items.push( "<li id='" + key + "'>" + val + "</li>" );
  });

  $( "<ul/>", {
    "class": "my-new-list",
    html: items.join( "" )
  }).appendTo( "body" );
});

$.getJSON() is a convenience method for .ajax() having some options prefilled.

Taken from their site.

As you see, the function passed in as a parameter is a callback (which is dealt with in the success-case of the $.getJSON()

The problem with that is, if you have several, i.e. sequential in time, operations going on, dealing with the what-ifs of what to do, when one of the operations is failing, becomes problematic to describe in code. The term for writing that code is callback hell.

To handle that problem in a more controllable and more readable way, there were promises introduced.

A promise represents the eventual result of an asynchronous operation.

Taken from Promises/A+.

A Promise is a proxy for a value not necessarily known when the promise is created.

Taken from MDN.

So making a function async is under the hood: wrapping it into a promise.

Coming back to await:

The await operator is used to wait for a Promise.

Taken from MDN.

Basically, await makes asynchronous functions itself "behave in a blocking way".


Taken from what was said above:

  • You are right in that these two functions behave similar in that respect that the execution from the perspective of the function itself happens synchronous.

  • You see the subtle difference when you throw in the callers perspective:

    Calling Function1 means start doSomething() and deal with the result later and proceed to the next step after the call of doSomething(). So calling doSomething() is non-blocking, even though the function in itself is blocking.

    Another subtle gotcha is hidden in Function1:

     const result = await doExpensiveOperation()
     const result2 = await doAnotherExpensiveOperation()
     return { result, result2 }
    

    The call to get result and result2 could be done asynchronously with await Promise.all().

  • Calling Function2 is blocking. Period. Whatever is done inside Function2 the caller has to wait.

4

Long story short, a GUI application is basically an infinite loop that processes events coming from the OS, the peripherals, etc. JavaScript running in a browser is no exception (see this). But, perhaps somewhat surprisingly, you'll find a similar thing in Node.js (see this). If a you have a long-running synchronous operation, it will block the loop, and your entire application will just freeze until that operation is done. So your Function 2 blocks the program. In the async version, an await basically just blocks the rest of that function, letting the main loop continue (the part below the await is really a callback in disguise). So your application remains responsive.

A call to an asynchronous operation returns essentially immediately, and it returns something called a Promise.
Note that you don't actually have to place the await keyword at the call site:

async function timeTest() {
  const timeoutPromise1 = timeoutPromise(3000);   // started
  const timeoutPromise2 = timeoutPromise(3000);   // started
  const timeoutPromise3 = timeoutPromise(3000);   // started

  // not yet blocked, you can do more stuff here while the 3 operations above
  // are running basically simultaneously "in the background"
  console.log('Processing...');
  
  // (but the main thing is that the rest of the 
  // program is allowed to continue working beyond this point; 
  // the location just before the first await is effectively the return point)

  // "block" & "wait"
  // (but really, call back, or "jump back into", the code below when the promises complete)
  await timeoutPromise1;
  await timeoutPromise2;
  await timeoutPromise3;
}

(example modified from here; see that article for more details)

The idea behind async/await was to make asynchronous programming look more like synchronous code.

1
4

The question hinges on whether the functions called in your example (doExpensiveOperation etc) are synchronous or asynchronous operations. Most JavaScript functions are synchronous. They do some stuff and return when they are finished. An asynchronous operation starts some background task (for example a network request), and then immediately returns - but the task runs to completion in the background and executes a callback function when finished.

Note that because JavaScript is single-threaded it is not possible to have JavaScript code execute in the background1. Asynchronous operations are built-in API functions like network requests or timers which can run outside of the JavaScript engine.

If the functions in your example are synchronous, then adding await has no effect whatsoever. The function is called, and when it returns, execution continues.

But if the functions are asynchronous we have an interesting difference. Lets use the example of the fetch API where a HTTP request is executed asynchronously. Example in pseudocode:

function 1

const promise = fetch('examplecom/api');
// rest of code

function 2

const result = await fetch('examplecom/api');
// rest of code

Now the difference is significant. In the first example, the code continues immediately after the request has been initiated. In the second example, the code waits until the response has been fully received. Which one to use depends on what behavior you want. If you are storing some data on the server, you might want to continue immediately since you don't need the response ("fire and forget"). If you are fetching some data, you might want to wait until it is received before you do something with the data.

The version with await is equivalent to passing a callback to be executed when the request is finished, like:

fetch('examplecom/api')
  .then((result) => {
    // rest of code;
  });

Callbacks like this quickly become tedious to write though, and await is introduced as syntactic sugar to allow you to write async code (almost) as straightforward as regular synchronous code.

Now imagine if there existed a synchronous counterpart to fetch called fetch_synchronous, which blocked until the request was complete. Code would look like this:

synchronous version

const result = fetch_synchronously('examplecom/api);
// rest of code

This looks very close to the version using await, and it also only continues when the result has been fetched. But there is a significant difference: Because JavaScript is single-threaded, when execution is blocked waiting for a synchronous function to complete, no other code can be executed - not even event handlers reacting to the user input. This means the whole page freezes and becomes unresponsive until the request is complete. This is obviously a bad user experience, and synchronous requests are therefore deprecated.

It is important to emphasize that await cannot turn a synchronous function in an asynchronous function or a blocking operation into a non-blocking operation. Therefore the use of await does not in itself make the application more responsive or faster or more concurrent. But if you have the choice between a synchronous and an asynchronous operation, using an asynchronous will increase responsiveness for the user.

(1) It is possible to have multithreaded JavaScript using WebWorkers, but this is a whole different can of worms, as they cannot be invoked with normal function calls.

1

In Javascript, within one worker, if you only use sync calls, you are guaranteed that the flow of execution will follow that of your code, and that nothing will interrupt it.

So in your second example, provided all of it involves only sync calls, then you know that it will call the first function, then the second function, and nothing else will have run in the meantime.

As soon as you have async calls (whether they use the older callback style, Promises, or async/await), this is no longer true. You start operations, and sometime later you get the result (either in the callback, or in a Promise then block, or via await), but other stuff may have happened in the meantime (callbacks or then blocks of other async operations, or a bit of code between two awaits).

Note that in many cases, it's not really up to you to decide whether you will use async/await or not: most calls involving I/O (network requests, database queries, file I/O...) are asynchronous, and you can't just call them and get a result without either a callback, a then block, or await (which options are available depend on the function). In some cases, you'll be perfectly happy getting a Promise and returning it to your caller, in many others you want the result before doing anything else, and you have to use one of those methods.

In other cases still, you'll want to run many of those asynchronous operations in parallel, in which case you can start them (call the functions), and wait for the results of all of them via Promise.all.

1

Not the answer you're looking for? Browse other questions tagged or ask your own question.