Some things in JavaScript take time — fetching data, reading a file, waiting for a timer. Rather than freezing the page, JavaScript starts them and carries on. Your code has to be written to cope with that.
The problem, shown in three lines#
console.log("first");
setTimeout(() => console.log("second"), 1000);
console.log("third");
// prints: first, third, secondJavaScript did not wait. It started the timer, moved on, and came back a second later. Everything about async programming follows from this.
What a promise is#
A promise is an object that stands for a result you do not have yet. It is in one of three states: pending, fulfilled, or rejected.
const promise = fetch("/api/users");
console.log(promise); // Promise { <pending> } — not the dataYou cannot use the value directly. You have to say what should happen once it arrives.
The old way: .then()#
fetch("/api/users")
.then(response => response.json())
.then(users => console.log(users))
.catch(error => console.log("Something went wrong:", error));This works, and you will see plenty of it. It gets awkward once steps depend on each other.
The readable way: async and await#
async function showUsers() {
try {
const response = await fetch("/api/users");
const users = await response.json();
console.log(users);
} catch (error) {
console.log("Something went wrong:", error);
}
}
showUsers();Two rules:
awaitpauses that function until the promise settles- You can only use
awaitinside a function markedasync
The code now reads top to bottom, like ordinary code, while still not blocking the page.
An async function always returns a promise#
async function getName() {
return "Ada";
}
console.log(getName()); // Promise { 'Ada' }
console.log(await getName()); // AdaThis is why you cannot call an async function and use its result directly — you have to await it too, or chain .then().
Waiting for several things at once#
// Slow — one after another
const users = await fetch("/api/users");
const posts = await fetch("/api/posts");
// Fast — both start immediately
const [users2, posts2] = await Promise.all([
fetch("/api/users"),
fetch("/api/posts"),
]);Use Promise.all when the requests do not depend on each other. If one fails, the whole thing rejects — Promise.allSettled gives you every result regardless.
Handling errors#
async function loadData() {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`Server said ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Could not load data:", error.message);
return null;
}
}Note that fetch does not throw on a 404 or 500. You have to check response.ok yourself — a genuine trap. See the Fetch API guide.
Questions people ask#
Does await block the whole page?
No. It pauses only the async function it is in. The rest of the page keeps responding, which is the entire point.
Can I use await at the top level of a file?
In modern JavaScript modules, yes. In a plain script, wrap it in an async function.
What is a callback, and how does it relate?
A callback is a function passed in to be run later. Promises were introduced because deeply nested callbacks became hard to read; async/await made promises easier again.
Why is my loop with await so slow?
Because for ... of with await runs one request at a time. Build an array of promises and pass it to Promise.all to run them together.