What are async/await in JavaScript and how do they differ from promises?
November 6, 2024
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:
- We mark the function as async
- We use await to pause at each step until it's complete
- 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:
- Simpler Syntax:
async/await
is much cleaner to read and write. No more.then()
chains - your code reads top to bottom. - Better Error handling: With
async/await
, you use normaltry/catch
blocks instead of.catch()
methods. Thetry/catch
pattern offers a more familiar approach to managing errors. - 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.