What are async/await in JavaScript and how do they differ from promises?

November 6, 2024

☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee

In modern web development, handling asynchronous operations efficiently is crucial for creating responsive applications. JavaScript has evolved from callback-based approaches to Promises, and now to async/await syntax.

Specifically, we will explore:

  • What is async/await in JavaScript?
  • Why did JavaScript introduce async/await syntax, and what problems does it solve?
  • What are the key differences between traditional Promise-based code and modern async/await implementations?
  • How async/await has transformed asynchronous programming in JavaScript.

What is async/await?

JavaScript runs in a single-threaded environment, meaning it can only execute one operation at a time. However, many operations in web development, such as fetching data from servers or reading files, take time to complete. These operations need to be handled asynchronously to prevent blocking the main thread. And the async/await keywords are designed to handle asynchronous operations.

Think of async/await as JavaScript's way of making complex timing problems simple. It's built on Promises, but it makes asynchronous code much easier to write and read. Instead of chaining .then() calls, you can write code that looks almost normal.

Let's break this down into two parts:

The async keyword

When you put async in front of a function, you're telling JavaScript two things: this function will take some time, and it will always return a Promise. Here's what that means in practice:

Here's a regular function that returns text immediately:

// Regular function - returns immediately
function f1() {
  return "Hello! ExplainThis!";
}

f1(); // Output: "Hello! ExplainThis!"

Now here's an async function that wraps its return value in a Promise:

// Async function - returns a Promise
async function f2() {
  return "Hello! ExplainThis!";
}

f2(); // Output: Promise {<fulfilled>: 'Hello! ExplainThis!'}

This is actually the same as writing:

// Another way to write the same thing
// Any function declared with async automatically returns a Promise, even when returning a simple value.
function f3() {
  return Promise.resolve("Hello! ExplainThis!");
}

f3(); // Output: Promise {<fulfilled>: 'Hello! ExplainThis!'}

To actually get the value out of an async function, you need to use .then():

async function f2() {
  return "Hello! ExplainThis!";
}

f2().then((result) => {
  console.log(result); // "Hello! ExplainThis!"
});

The await keyword

await is where the magic happens. It lets you pause your code until a Promise resolves, making asynchronous code look synchronous. You can only use await inside async functions (with one exception we'll get to in a moment).

Here's a real-world example:

async function getData() {
  // Wait for the network request to complete
  const res = await fetch("<https://example.com/data>");

  // Wait for the JSON parsing to complete
  const data = await res.json();

  // Now we can use the data
  console.log(data);
}

getData();

Important things to know about await:

You can't use await in regular functions:

function f() {
  let promise = Promise.resolve("Hello! ExplainThis!");
  let result = await promise; // This will fail!
}

// Error: await is only valid in async functions and at the top level of modules

Top-level await: in modern JavaScript modules, you can use await at the top level. This is really useful when setting up your application. Instead of this:

import getData from "./getData.js";

let data;

getData().then((result) => {
  data = result;
  // use data here
});

Then, you can write this:

const data = await getData();
// use data here

How to use async/await effectively?

Let's look at a real example. First, here's how we might fetch data using Promises:

function getData(url) {
  return new Promise((resolve, reject) => {
    fetch(url)
      .then((res) => res.json())
      .then((data) => resolve(data))
      .catch((error) => reject(error));
  });
}

getData("<https://example.com/data>")
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

Now here's the same thing with async/await:

async function getData(url) {
  try {
    const res = await fetch(url);
    const data = await res.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

getData("<https://example.com/data>");

In this version:

  1. We mark the function as async
  2. We use await to pause at each step until it's complete
  3. We use normal try/catch for errors - much cleaner!

How async/await differs from Promises?

While async/await and Promises do the same job, they're different in important ways:

  1. Simpler Syntax: async/await is much cleaner to read and write. No more .then() chains - your code reads top to bottom.
  2. Better Error handling: With async/await, you use normal try/catch blocks instead of .catch() methods. The try/catch pattern offers a more familiar approach to managing errors.
  3. Improved Readability: async/await makes asynchronous code read like synchronous code - it's much easier to follow what's happening. This makes code easier to understand and maintain.

The introduction of async/await represents a significant step forward in JavaScript's evolution, making asynchronous programming more accessible while maintaining the language's powerful capabilities for handling concurrent operations.

☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee