Despite all the fancy features baked into modern programming languages, under the hood, they’re still fairly primitive. Without asynchronous programming, long calls to external services would hault program execution. Async/await fixes that issue.
Why Use Async/Await?
Most programs use one main thread and execute CPU instructions sequentially, but more importantly, synchronously. If you have to fetch a value from memory, you are stuck in IOWait for a bit, while waiting for your RAM to return a response. If you need to fetch a value from a disk, you might be stuck for a while (hence, why RAM is so useful).
But, if you’re fetching something from a remote server, now you have a big problem. It could take 5 seconds or more for the server to return a response, and on a single-threaded program, that would block main execution, causing your page to become unresponsive and your users to leave.
Asynchronous programming is here to save the users. In JavaScript, the most basic form of async code is passing in a callback, usually an anonymous function, that will run after the web request has been completed. You can do this using Ajax as well as other request libraries.
This can get very messy when dealing with multiple concurrent requests and lots of functions, so JavaScript created Promises. These are wrapper objects that represent async variables that are still fetching their values. Promises can be returned when using functions like JavaScript’s built in fetch. You can register a callback function using .then(), which allows you to chain together multiple requests.
Returning an object like this allows the use of other features like Promise.All, which can execute multiple promises and run a single callback after all of them complete.
This is a huge step up, but it’s really just moving the problem. Writing code like this is still counterintuitive, having to manage all the running promises and make sure everything is in order. If the rest of your code depends on the result of the promise, you’ll need to move that code into the .then block, or call a separate function with the data.
Ideally, it would be best to just not have to bother with promises and callbacks. You’d like to write code like this, but JavaScript will either halt program execution, or run it out of order.
But, there is a better way—async/await. You’ll still need to work with promises at the end of the day, but async/await offers a better syntax for using them.
To use it, you’ll need to specify the function as an async function. This ensures that the function returns a promise that JavaScript can act on when calling this function. The real magic happens with await, which you can add before a Promise to tell the JavaScript engine that this function is going to take a bit, and it should pause your async function until it returns, freeing up time to do other actions and to keep your application running.
Now, everything in the async block runs in order but doesn’t halt program execution to do so. Anything coming after fetchAPI() will still run out of order, as fetchAPI() is simply starting the execution of an async function.
You can essentially think of await as something you’ll prepend to web requests and other things that take a while to return a response. It can only be used in an async function, and you use it to explicitly await a function that returns a promise, rather than calling .Then() and passing a callback.
Async/Await in Other Languages
Async/Await isn’t just a JavaScript concept; it’s a feature in many other programming languages, though the exact implementation may differ a bit.
For example, C# has Tasks, which function like Promises and represent an async operation that returns something. You can await other tasks inside the async function, allowing for non-blocking code for operations like fetching web requests and writing/reading from disk or external datastores like S3 buckets.
You can chain these together using .ContinueWith(callback):
Depending on your application, you might have to check repeatedly in the main thread if the task has been completed. For example, you might have a co-routine that runs once per frame, and checks if the Task has finished with the .IsCompleted field. If it isn’t, it yields until next frame, and if it is, it can continue processing synchronously.