Promises, Async/Await
Introduction: Callbacks
Callbacks are functions passed as arguments to other functions, to be executed later.
function loadScript(src, callback) {
let script = document.createElement("script");
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error("Failed to load: " + src));
document.head.append(script);
}
// Usage
loadScript("script.js", (err, script) => {
if (err) {
console.log("Error:", err.message);
} else {
console.log("Script loaded:", script.src);
}
});
Callback Hell
When callbacks are nested, readability suffers:
// β Callback hell / Pyramid of doom
loadScript("a.js", function(err) {
if (!err) {
loadScript("b.js", function(err) {
if (!err) {
loadScript("c.js", function(err) {
if (!err) {
loadScript("d.js", function(err) {
// ... and so on
});
}
});
}
});
}
});
Promises solve this problem elegantly.
Promise
A Promise represents a value that may be available now, later, or never.
Promise States
- pending β initial state
- fulfilled β operation completed successfully
- rejected β operation failed
Creating a Promise
let promise = new Promise((resolve, reject) => {
// Async operation
setTimeout(() => {
let success = true;
if (success) {
resolve("Operation succeeded");
} else {
reject(new Error("Operation failed"));
}
}, 1000);
});
Consuming a Promise
promise.then(
(result) => console.log("Success:", result),
(error) => console.log("Error:", error.message)
);
// Or more clearly:
promise
.then((result) => console.log("Success:", result))
.catch((error) => console.log("Error:", error.message))
.finally(() => console.log("Always runs"));
Example: loadScript with Promise
function loadScript(src) {
return new Promise((resolve, reject) => {
let script = document.createElement("script");
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error("Failed: " + src));
document.head.append(script);
});
}
loadScript("script.js")
.then((script) => console.log("Loaded:", script.src))
.catch((err) => console.log("Error:", err.message));
Immediately Settled Promises
Promise.resolve("Done"); // Creates a fulfilled promise
Promise.reject(new Error("")); // Creates a rejected promise
function wait(ms, value) {
return new Promise(resolve => {
setTimeout(() => resolve(value), ms);
});
}
// Chain three async operations
wait(1000, 'First')
.then(result => {
document.write(result + ' (after 1s)<br>');
return wait(1000, 'Second');
})
.then(result => {
document.write(result + ' (after 2s)<br>');
return wait(1000, 'Third');
})
.then(result => {
document.write(result + ' (after 3s)<br>');
document.write('All done!');
}); Promises Chaining
Each .then() returns a new promise, allowing chaining:
new Promise((resolve) => {
setTimeout(() => resolve(1), 1000);
})
.then((result) => {
console.log(result); // β 1
return result * 2;
})
.then((result) => {
console.log(result); // β 2
return result * 2;
})
.then((result) => {
console.log(result); // β 4
return result * 2;
})
.then((result) => {
console.log(result); // β 8
});
Returning Promises from .then()
new Promise((resolve) => {
setTimeout(() => resolve(1), 1000);
})
.then((result) => {
console.log(result); // β 1
return new Promise((resolve) => {
setTimeout(() => resolve(result * 2), 1000);
});
})
.then((result) => {
console.log(result); // β 2
return new Promise((resolve) => {
setTimeout(() => resolve(result * 2), 1000);
});
})
.then((result) => {
console.log(result); // β 4
});
thenable objects
Any object with a .then() method is treated as a promise:
class Thenable {
constructor(value) {
this.value = value;
}
then(resolve) {
setTimeout(() => resolve(this.value * 2), 1000);
}
}
Promise.resolve(5)
.then(result => new Thenable(result))
.then(console.log); // β 10 (after 1s)
Error Handling with Promises
.catch() catches errors anywhere in the chain
new Promise((resolve, reject) => {
throw new Error("Whoops!");
})
.catch((err) => console.log("Caught:", err.message)); // β "Caught: Whoops!"
// Same as:
new Promise((resolve, reject) => {
reject(new Error("Whoops!"));
}).catch((err) => console.log("Caught:", err.message));
Implicit tryβ¦catch
Promise executor has an implicit try...catch:
new Promise((resolve, reject) => {
// This throw is caught and turns into a rejection
throw new Error("Error in executor");
}).catch(console.log);
// Same as:
new Promise((resolve, reject) => {
reject(new Error("Error in executor"));
}).catch(console.log);
Errors in .then()
Promise.resolve(5)
.then((result) => {
throw new Error("Error in then"); // Caught by next catch
})
.catch((err) => console.log(err.message));
Rethrowing
If you catch and rethrow, the next .catch() handles it:
Promise.resolve(5)
.then((result) => {
throw new Error("First error");
})
.catch((err) => {
console.log("Caught:", err.message);
throw err; // Rethrow
})
.catch((err) => {
console.log("Caught again:", err.message);
});
// β "Caught: First error"
// β "Caught again: First error"
Unhandled Rejections
If a promise rejects and thereβs no .catch(), itβs an unhandled rejection:
// Browser
window.addEventListener("unhandledrejection", (event) => {
console.log("Unhandled rejection:", event.reason);
});
// Node.js
process.on("unhandledRejection", (err) => {
console.error("Unhandled rejection:", err);
});
// Always add .catch() at the end of chains!
Good Error Handling Pattern
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
getUser(42)
.then(user => console.log(user.name))
.catch(err => {
console.error("Failed to get user:", err.message);
// Show user-friendly error message
});
Promise API
Promise.all
Waits for all promises to fulfill, or rejects immediately if any fails:
let p1 = Promise.resolve(3);
let p2 = new Promise(resolve => setTimeout(() => resolve("done"), 1000));
let p3 = fetch("/api/data").then(r => r.json());
Promise.all([p1, p2, p3]).then((results) => {
console.log(results); // [3, "done", data] β all results in order
});
// If any rejects, Promise.all rejects with that error
Promise.all([
Promise.resolve(1),
Promise.reject(new Error("Fail")),
Promise.resolve(3)
]).catch(err => console.log(err.message)); // β "Fail"
Promise.allSettled (ES2020)
Waits for all promises to settle (fulfilled or rejected):
Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error("Fail")),
Promise.resolve(3)
]).then((results) => {
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("Value:", result.value);
} else {
console.log("Reason:", result.reason.message);
}
});
});
// β "Value: 1"
// β "Reason: Fail"
// β "Value: 3"
Promise.race
Returns the first settled promise (fulfilled or rejected):
let fast = new Promise(resolve => setTimeout(() => resolve("fast"), 500));
let slow = new Promise(resolve => setTimeout(() => resolve("slow"), 2000));
Promise.race([fast, slow]).then(console.log); // β "fast" (after 500ms)
// Useful for timeouts:
let data = Promise.race([
fetch("/api/data"),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), 5000)
)
]);
Promise.any (ES2021)
Returns the first fulfilled promise (ignores rejections):
Promise.any([
Promise.reject(new Error("Fail 1")),
Promise.resolve(2),
Promise.resolve(3)
]).then(console.log); // β 2 (first fulfilled)
// If all reject, returns AggregateError
Promise.any([
Promise.reject(new Error("Fail 1")),
Promise.reject(new Error("Fail 2"))
]).catch(err => {
console.log(err instanceof AggregateError); // β true
console.log(err.errors); // β [Error, Error]
});
Promise.resolve / Promise.reject
Promise.resolve(42).then(console.log); // β 42
Promise.reject(new Error("Bad")).catch(console.log);
function delay(ms, value) {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}
// Promise.all
Promise.all([
delay(1000, 'Task 1 (1s)'),
delay(500, 'Task 2 (0.5s)'),
delay(1500, 'Task 3 (1.5s)')
]).then(results => {
document.write('Promise.all results:<br>');
results.forEach(r => document.write('- ' + r + '<br>'));
});
// Promise.race
Promise.race([
delay(2000, 'Slow'),
delay(1000, 'Fast')
]).then(result => {
document.write('<br>Promise.race winner: ' + result + '<br>');
});
// Promise.any
Promise.any([
Promise.reject(new Error('Fails')),
delay(800, 'First success')
]).then(result => {
document.write('<br>Promise.any result: ' + result + '<br>');
}); Promisification
Converting a callback-based function to return a promise:
// Callback-based
function readFile(path, encoding, callback) {
// ... reads file, calls callback(err, data)
}
// Promisified
function readFilePromise(path, encoding) {
return new Promise((resolve, reject) => {
readFile(path, encoding, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// Usage
readFilePromise("/path/to/file", "utf-8")
.then((data) => console.log(data))
.catch((err) => console.error(err));
General Promisification Function
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn.call(this, ...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
}
let readFileAsync = promisify(readFile);
let fs = require("fs");
let readFile = promisify(fs.readFile);
// Node.js has built-in promisify
const { promisify } = require("util");
let readFile = promisify(require("fs").readFile);
Microtasks
Event Loop
JavaScript has an event loop that processes:
- Macrotasks β setTimeout, setInterval, I/O, UI rendering
- Microtasks β Promise.then/catch/finally, queueMicrotask, MutationObserver
console.log("Start");
setTimeout(() => console.log("Timeout"), 0); // Macrotask
Promise.resolve()
.then(() => console.log("Promise 1")) // Microtask
.then(() => console.log("Promise 2")); // Microtask
console.log("End");
// Output:
// β "Start"
// β "End"
// β "Promise 1"
// β "Promise 2"
// β "Timeout"
Microtasks Queue
Microtasks are processed before the next macrotask:
setTimeout(() => console.log("Timeout 1"), 0);
setTimeout(() => console.log("Timeout 2"), 0);
Promise.resolve().then(() => {
console.log("Promise");
// This adds another microtask
Promise.resolve().then(() => console.log("Promise nested"));
});
console.log("Synchronous");
// β "Synchronous"
// β "Promise"
// β "Promise nested"
// β "Timeout 1"
// β "Timeout 2"
queueMicrotask
queueMicrotask(() => {
console.log("This runs as a microtask");
});
Why This Matters
Promise handlers run before DOM rendering and before setTimeout callbacks:
// DOM updates happen after microtasks
document.getElementById("loading").textContent = "Loading...";
fetch("/api/data")
.then(data => {
// Microtask β runs before rendering
// DOM isn't updated yet from the line above
});
Async/Await
async/await makes asynchronous code look synchronous.
async function
An async function always returns a promise:
async function greet() {
return "Hello";
}
let result = greet();
console.log(result); // β Promise {<fulfilled>: "Hello"}
// Equivalent to:
function greet() {
return Promise.resolve("Hello");
}
await
await pauses execution until the promise settles:
async function getData() {
let response = await fetch("/api/data");
let data = await response.json();
console.log(data);
return data;
}
// The function is paused at each await
// Other code can run during the wait
Async/await vs Promise Chains
// Promise chain
function getWeather(city) {
return fetch(`/api/weather/${city}`)
.then(response => response.json())
.then(data => data.temperature)
.catch(err => console.error("Failed:", err));
}
// Async/await β much cleaner
async function getWeather(city) {
try {
let response = await fetch(`/api/weather/${city}`);
let data = await response.json();
return data.temperature;
} catch (err) {
console.error("Failed:", err);
}
}
Sequential vs Parallel
// Sequential (one after another)
async function sequential() {
let user = await getUser(1);
let posts = await getPosts(user.id);
let comments = await getComments(posts[0].id);
return comments;
}
// Parallel (all at once)
async function parallel() {
let [user, posts, comments] = await Promise.all([
getUser(1),
getPosts(1),
getComments(1)
]);
return { user, posts, comments };
}
Error Handling with Async/Await
async function processUser(id) {
try {
let user = await fetch(`/api/users/${id}`);
return await user.json();
} catch (err) {
console.error("Error:", err.message);
throw err; // Re-throw if needed
}
}
// Or use .catch() on the returned promise
processUser(42).catch(err => console.error("Handled outside:", err));
Async/Await with Loops
// Sequential processing with for...of
async function processItems(items) {
for (let item of items) {
await process(item); // One at a time
}
}
// Parallel processing with map
async function processItems(items) {
await Promise.all(items.map(item => process(item))); // All at once
}
// Important: forEach doesn't work with await!
// items.forEach(async (item) => { await process(item); }); // β
Top-level await (Modules)
In ES modules, await works at the top level:
<script type="module">
let response = await fetch("/api/data");
let data = await response.json();
console.log(data);
</script>
// In ES module files
import { fetch } from "./api.js";
let config = await fetch("/api/config").then(r => r.json());
export default config;
// Simulated async functions
function fetchUser(id) {
return new Promise(resolve => {
setTimeout(() => resolve({ id, name: 'User ' + id }), 1000);
});
}
function fetchPosts(userId) {
return new Promise(resolve => {
setTimeout(() => resolve(['Post 1', 'Post 2', 'Post 3']), 800);
});
}
// Using async/await
async function loadUserProfile(userId) {
try {
document.write('Loading user...<br>');
let user = await fetchUser(userId);
document.write('User: ' + user.name + '<br>');
document.write('Loading posts...<br>');
let posts = await fetchPosts(user.id);
document.write('Posts: ' + posts.join(', ') + '<br>');
document.write('Profile loaded successfully!');
} catch (err) {
document.write('Error: ' + err.message);
}
}
loadUserProfile(42);