JavaScript Async/Await in Depth
You met promises and async/await in the async lesson. This lesson shows how to run many tasks together, handle their errors, and cancel them.
A Helper for the Examples
Real requests need a server, so these examples use a fake one. fakeRequest waits for some time, then succeeds or fails, just like a real network call.
Example
function fakeRequest(name, ms, fail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (fail) reject(new Error(name + " failed"));
else resolve(name + " loaded");
}, ms);
});
}Sequential vs Parallel
If you await one task after another, each waits for the one before. If the tasks do not depend on each other, that wastes time.
Example
<!DOCTYPE html>
<html>
<body>
<p id="demo">Working...</p>
<script>
function fakeRequest(name, ms) {
return new Promise(resolve => setTimeout(() => resolve(name), ms));
}
async function run() {
const out = document.getElementById("demo");
let t = performance.now();
await fakeRequest("profile", 1000);
await fakeRequest("courses", 1000);
const slow = Math.round((performance.now() - t) / 1000);
t = performance.now();
await Promise.all([fakeRequest("profile", 1000), fakeRequest("courses", 1000)]);
const fast = Math.round((performance.now() - t) / 1000);
out.innerHTML = "One after another: about " + slow + " s<br>Together with Promise.all: about " + fast + " s";
}
run();
</script>
</body>
</html>Try it Yourself »One after another: about 2 s Together with Promise.all: about 1 s
Start tasks together when they are independent. Only await them one by one when the second task needs the result of the first.
The Four Promise Combinators
| Method | Waits for | Result |
|---|---|---|
Promise.all(list) | All to succeed | Array of results. Rejects as soon as one fails |
Promise.allSettled(list) | All to finish | Array of { status, value } or { status, reason }, never rejects |
Promise.race(list) | The first to finish | That one's result or error |
Promise.any(list) | The first to succeed | Its result. Rejects only if all fail |
Promise.all With Destructuring
Promise.all keeps the results in the same order as the list, so destructuring works nicely:
Example
async function loadDashboard() {
const [user, orders, offers] = await Promise.all([
fakeRequest("user", 800),
fakeRequest("orders", 1200),
fakeRequest("offers", 500)
]);
console.log(user, "|", orders, "|", offers);
}
loadDashboard();user loaded | orders loaded | offers loaded
Promise.allSettled: Do Not Fail Everything
If one widget on a page fails, the others should still show. allSettled reports each result separately:
Example
<!DOCTYPE html>
<html>
<body>
<ul id="list"></ul>
<script>
function fakeRequest(name, ms, fail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => fail ? reject(new Error(name + " failed")) : resolve(name + " loaded"), ms);
});
}
async function loadWidgets() {
const results = await Promise.allSettled([
fakeRequest("Weather", 600),
fakeRequest("Cricket score", 900, true),
fakeRequest("Petrol price", 400)
]);
for (const r of results) {
const li = document.createElement("li");
li.textContent = r.status === "fulfilled" ? r.value : "Error: " + r.reason.message;
document.getElementById("list").append(li);
}
}
loadWidgets();
</script>
</body>
</html>Try it Yourself »Weather loaded Error: Cricket score failed Petrol price loaded
Error Handling Patterns
Wrap await in try...catch. Remember that an async function always returns a promise, so errors inside it become a rejected promise for the caller.
Example
async function getStudent(id) {
const response = await fetch("/api/students/" + id);
if (!response.ok) throw new Error("HTTP " + response.status);
return response.json();
}
async function showStudent() {
try {
const student = await getStudent(7);
console.log(student.name);
} catch (error) {
console.log("Could not load student:", error.message);
} finally {
console.log("Hide the loading spinner");
}
}A common mistake is forgetting await. Then try...catch cannot catch the error, because the promise fails after the try block has ended.
Cancelling With AbortController
Sometimes you need to stop a request: the user typed a new search, left the page, or the server is too slow. An AbortController gives you a signal to pass to fetch, and an abort() method to cancel it.
Example
const controller = new AbortController();
fetch("https://jsonplaceholder.typicode.com/posts", { signal: controller.signal })
.then(res => res.json())
.then(posts => console.log(posts.length))
.catch(err => {
if (err.name === "AbortError") console.log("Request was cancelled");
else console.log("Other error:", err.message);
});
controller.abort(); // cancel it straight awayRequest was cancelled
Timeouts With AbortSignal.timeout
For a time limit, you do not even need a controller. AbortSignal.timeout(ms) makes a signal that aborts itself:
Example
<!DOCTYPE html>
<html>
<body>
<button id="load">Load posts (2 second limit)</button>
<p id="demo"></p>
<script>
document.getElementById("load").addEventListener("click", async () => {
const out = document.getElementById("demo");
out.textContent = "Loading...";
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
signal: AbortSignal.timeout(2000)
});
const posts = await res.json();
out.textContent = "Loaded " + posts.length + " posts.";
} catch (err) {
out.textContent = err.name === "TimeoutError"
? "The server took too long. Please try again."
: "Could not load: " + err.message;
}
});
</script>
</body>
</html>Try it Yourself »A timeout signal fails with a TimeoutError, while a manual abort() fails with an AbortError.
Top-level await
In a module (<script type="module">), you can use await directly, outside any function:
Example
// inside <script type="module">
const res = await fetch("/config.json");
const config = await res.json();Exercise
Exercise
You load three independent widgets and want to show every one that works, even if one fails. Which method fits best?
