async and wait in 5 Levels

Let’s break down the concept of async and await in JavaScript:

1. To a Child:

Imagine you’re playing a video game, and you reach a point where you need to wait for an upgrade to happen. You don’t want to pause the whole game while waiting, right? You want to continue doing other things in the game. async and await in JavaScript is like that. It helps the computer do other tasks without having to wait for slower tasks (like upgrading your game character) to finish.

2. To a Teenager:

You know when you’re on a web page and you click on something, and while the computer is getting what you clicked on, you can still scroll or click on other things? That’s because of something called “asynchronous programming.” async and await are keywords in JavaScript that make this easier. async tells JavaScript, “Hey, this function won’t finish right away,” and await says, “Wait here until this thing is done before moving on.”

3. To an Undergraduate Computer Science Major:

async and await are part of JavaScript’s features for dealing with asynchronous operations. async is used to declare a function as asynchronous, meaning it’s expected to involve operations that won’t complete instantly, like fetching data from a server. await is used within async functions to pause the execution of the function until a Promise is resolved or rejected, making it easier to write asynchronous code that looks and behaves more like synchronous code.

4. To a Graduate Computer Science Student:

async and await are syntactic sugar in JavaScript for handling Promises, providing a more straightforward way to write asynchronous code compared to callbacks or raw Promises. An async function implicitly returns a Promise and await is used to pause execution until that Promise is resolved or rejected. Exception handling can be done using try-catch blocks, just like synchronous code. This greatly simplifies the logic around concurrent operations and makes the code more readable.

5. To a Colleague (Professional Developer):

In JavaScript, async and await are used for handling asynchronous operations in a more linear, easy-to-read fashion, reducing the so-called “callback hell”. When marked as async, a function returns a Promise and any exceptions thrown within the function are turned into rejections. The await keyword can only be used inside an async function and causes the function execution to pause and wait for a Promise’s resolution or rejection. It helps write cleaner, more intuitive asynchronous code and simplifies error handling by allowing for conventional try/catch blocks. However, care must be taken not to overuse await as it can lead to sequential execution of asynchronous operations, negating the benefits of asynchronous code.

Code

The async and await keywords allow us to write asynchronous code that is Promise-based under the hood but that looks like synchronous code. This makes the code easier to understand and reason about. If a function is declared async, it will implicitly return a Promise. Inside an async function, you can await a Promise (or a function that returns a Promise) as if the Promise value was synchronously computed.

The async and await keywords allow you to write Promise-based asynchronous code in a way that appears synchronous. It’s a fantastic feature that can really help to simplify complex async flows.

Here’s an example demonstrating how promises can be used and then I’ll provide the async/await version of the same example. Here’s the Promise version:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const fs = require('fs').promises;
const request = require('request-promise');

function uploadFile() {
  fs.readFile('file.txt', 'utf8')
    .then(data => request.post('http://mywebsite.com/upload', { body: data }))
    .then(() => console.log('Upload successful!'))
    .catch(err => console.error('An error occurred', err));
}

uploadFile();

In this version, we’re using fs.readFile() to read the contents of a file. This returns a Promise, which we can attach a then() callback to. When the Promise resolves, the callback is called with the file data.

We’re then returning another Promise from inside that callback (from the request.post() call). This allows us to chain another then() to the end, which will run when the POST request is complete.

Finally, we have a catch() call at the end of the chain. This will catch any errors that occur during any of the asynchronous steps.

And here’s the async/await version:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const fs = require('fs').promises;
const request = require('request-promise');

async function uploadFile() {
  try {
    const data = await fs.readFile('file.txt', 'utf8');
    await request.post('http://mywebsite.com/upload', { body: data });
    console.log('Upload successful!');
  } catch (err) {
    console.error('An error occurred', err);
  }
}

uploadFile();

In this version, we’ve created an async function uploadFile(). Within this function, we’re using await to pause the execution until the Promise from fs.readFile() is fulfilled. Once it’s done, we’re storing the result in data.

Then, we’re using await again to pause the execution until the Promise from request.post() is fulfilled. If any of these Promises is rejected, the error will be caught by the catch block.

Note that the await keyword can only be used inside an async function. In the global scope of a module, you cannot use await by itself. That’s why we wrapped the code inside the uploadFile() function and then we called it. The function returns a Promise, but since we don’t need to handle the result or error at the global scope in this case, we just called the function without then or catch.

Design Considerations

Promises and async/await can be used effectively and both have their own use cases. However, some considerations might help in deciding which to use:

  1. Readability and Structure: Async/await generally results in cleaner and more readable code than Promises. It’s often easier to follow the flow of execution with async/await, since it allows asynchronous code to be written in a more synchronous style.

  2. Error Handling: Both provide mechanisms for error handling, but async/await is arguably easier because it allows the use of standard try/catch blocks, which aren’t available with Promises.

  3. Concurrence and Performance: Promises can be more efficient when handling multiple concurrent operations. For example, with Promise.all() you can execute multiple Promises at the same time and wait for all to resolve. While you can do this with async/await as well, the syntax is a bit less clear.

  4. Flexibility: Promises have a number of utility methods (like Promise.all(), Promise.race(), etc.) that can be very useful depending on your needs. These are not directly available with async/await, although you can still use them in conjunction with async/await.

In summary, the choice often comes down to personal preference and the specific needs of the project. In general, if you’re working with a few asynchronous operations that are dependent on each other, async/await might be the better choice. But if you have multiple independent async operations and want to manage them concurrently, Promises could be more suitable.