Async/Await is syntactic sugar built on top of Promises that makes asynchronous code look and behave more like synchronous code. Introduced in ES2017 (ES8), it has become the standard way to write asynchronous JavaScript. If you understand Promises, async/await is easy to learn — it simply provides a cleaner syntax for the same underlying mechanism.
Placing async before a function declaration makes it automatically return a Promise.
async function greet() {
return "Hello!";
}
// Equivalent to:
function greet() {
return Promise.resolve("Hello!");
}
greet().then(message => console.log(message)); // "Hello!"const greet = async () => "Hello!";
const fetchUser = async (id) => ({ id, name: "Alice" });const api = {
async getUser(id) {
return { id, name: "Alice" };
}
};await can only be used inside an async function. It pauses execution until the Promise resolves, then returns its value.
async function getData() {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
return data;
}What happens:
fetch()returns a PromiseawaitpausesgetData()execution- When the Promise resolves,
awaitreturns the response - Execution continues to the next line
response.json()returns another Promiseawaitpauses again- When resolved,
datacontains the parsed JSON
Important: While
awaitpauses the function execution, it does NOT block the main thread. Other code can run during the wait.
function fetchUserData(userId) {
return fetchUser(userId)
.then(user => fetchOrders(user.id))
.then(orders => fetchProducts(orders[0].id))
.then(products => ({ user, orders, products }))
.catch(error => {
console.error("Error:", error);
throw error;
});
}async function fetchUserData(userId) {
try {
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
const products = await fetchProducts(orders[0].id);
return { user, orders, products };
} catch (error) {
console.error("Error:", error);
throw error;
}
}Benefits of async/await:
- Reads like synchronous code
- Easier to debug (stack traces are clearer)
- Error handling with
try/catchfeels natural - No nesting or chaining needed
async function getUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error("Failed to fetch user:", error.message);
return null;
}
}async function processPayment(orderId) {
try {
const order = await fetchOrder(orderId);
const payment = await chargeCustomer(order.total);
return payment;
} catch (error) {
// Log and re-throw for caller to handle
console.error("Payment failed:", error);
throw new PaymentError("Could not process payment", { cause: error });
}
}async function complexOperation() {
let user;
try {
user = await fetchUser(1);
} catch (error) {
console.error("User fetch failed, using default");
user = { id: 0, name: "Guest" };
}
let orders;
try {
orders = await fetchOrders(user.id);
} catch (error) {
console.error("Orders fetch failed");
orders = [];
}
return { user, orders };
}async function sequentialFetch() {
const user = await fetchUser(1); // Wait 1s
const orders = await fetchOrders(1); // Wait 1s
const products = await fetchProducts(1); // Wait 1s
// Total: ~3 seconds
return { user, orders, products };
}async function parallelFetch() {
const [user, orders, products] = await Promise.all([
fetchUser(1),
fetchOrders(1),
fetchProducts(1)
]);
// Total: ~1 second
return { user, orders, products };
}async function smartFetch(userId) {
const user = await fetchUser(userId);
// Only fetch orders if user is active
let orders = [];
if (user.isActive) {
orders = await fetchOrders(userId);
}
return { user, orders };
}async function processUsers(userIds) {
const results = [];
for (const id of userIds) {
const user = await fetchUser(id);
results.push(user);
}
return results;
}
// Each fetch waits for the previous to completeasync function processUsersParallel(userIds) {
const users = await Promise.all(
userIds.map(id => fetchUser(id))
);
return users;
}
// All fetches run simultaneously// ❌ WRONG — forEach doesn't wait for async callbacks
async function badProcess(users) {
users.forEach(async (user) => {
await saveUser(user); // Fire and forget!
});
console.log("Done!"); // This prints before saves complete
}
// ✅ CORRECT — Use for...of
async function goodProcess(users) {
for (const user of users) {
await saveUser(user);
}
console.log("Done!"); // This prints after all saves
}
// ✅ CORRECT — Use Promise.all with map
async function alsoGoodProcess(users) {
await Promise.all(users.map(user => saveUser(user)));
console.log("Done!");
}Modern JavaScript supports await at the top level of modules (ES modules):
// config.mjs
const config = await fetch("/api/config").then(r => r.json());
export default config;// main.mjs
import config from "./config.mjs";
console.log(config); // Already resolved!In Node.js, use
.mjsextension or"type": "module"in package.json.
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = 1000 * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} finally {
clearTimeout(timeout);
}
}async function pollForResult(jobId, intervalMs = 1000, maxAttempts = 30) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const result = await checkJobStatus(jobId);
if (result.status === "completed") {
return result.data;
}
if (result.status === "failed") {
throw new Error("Job failed");
}
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
throw new Error("Polling timeout");
}async function processTasks(tasks) {
const results = [];
const errors = [];
for (const task of tasks) {
try {
const result = await task();
results.push(result);
} catch (error) {
errors.push({ task, error });
}
}
return { results, errors };
}// Callback version
function getUserCallback(id, callback) {
setTimeout(() => {
callback(null, { id, name: "Alice" });
}, 100);
}
// Promise wrapper
function getUserPromise(id) {
return new Promise((resolve, reject) => {
getUserCallback(id, (error, user) => {
if (error) reject(error);
else resolve(user);
});
});
}
// Async/await usage
async function main() {
const user = await getUserPromise(1);
console.log(user);
}// ❌ Missing await
async function getData() {
const response = fetch("/api/data"); // Returns a Promise!
const data = response.json(); // Error! Can't call .json() on a Promise
}
// ✅ Correct
async function getData() {
const response = await fetch("/api/data");
const data = await response.json();
}// ❌ Unnecessary mixing
async function getUser() {
return await fetchUser(1).then(user => {
return user.name;
});
}
// ✅ Cleaner
async function getUser() {
const user = await fetchUser(1);
return user.name;
}// ❌ Returns undefined on error
async function getUserSafe(id) {
try {
return await fetchUser(id);
} catch (error) {
console.error(error);
// Forgot to return anything!
}
}
// ✅ Return a fallback
async function getUserSafe(id) {
try {
return await fetchUser(id);
} catch (error) {
console.error(error);
return { id: 0, name: "Guest" };
}
}// ❌ Pointless async
async function getNumber() {
return 42;
}
// ✅ Just return the value
function getNumber() {
return 42;
}
// ✅ Only use async when there's await inside
async function getUserData(id) {
const user = await fetchUser(id);
return user;
}// ❌ In a regular function, this promise is floating
function process() {
saveData(); // Promise is created but not handled!
}
// ✅ Handle the promise
function process() {
saveData().catch(error => console.error(error));
}
// ✅ Or make the function async
async function process() {
await saveData();
}Convert this Promise chain to async/await:
function loadDashboard(userId) {
return fetchUser(userId)
.then(user => fetchSettings(user.id).then(settings => ({ user, settings })))
.then(({ user, settings }) => fetchWidgets(settings.theme).then(widgets => ({ user, settings, widgets })))
.then(dashboard => {
console.log("Dashboard loaded");
return dashboard;
})
.catch(error => {
console.error("Failed to load dashboard:", error);
return null;
});
}async function delay(ms) {
// Your code
}
async function main() {
console.log("Start");
await delay(1000);
console.log("After 1 second");
}Fetch posts and then fetch comments for each post sequentially.
async function loadPostsWithComments() {
const posts = await fetchPosts();
// For each post, fetch its comments
// Return array of posts with comments included
}Fetch URLs in parallel but limit to 3 concurrent requests.
async function fetchWithLimit(urls, concurrency = 3) {
// Your code
}Implement retry with random delays to avoid thundering herd.
async function fetchWithJitterRetry(url, maxRetries = 3) {
// Add random delay between retries
}asyncmakes a function return a Promise automaticallyawaitpauses execution until a Promise resolves- Async/await code reads like synchronous code but doesn't block the main thread
- Use
try/catchfor error handling — it's more intuitive than.catch() - Use
Promise.all()for parallel execution inside async functions - Use
for...offor sequential loops, neverforEachwith async callbacks - Always await Promise-returning functions
- Add
.catch()to floating promises in non-async contexts - Top-level
awaitis available in ES modules
You're now ready for:
- Converting Callbacks to Promises/Async — modernizing legacy code
- Testing Async Code — Jest async test patterns
- Error Handling Patterns — production-ready strategies
Happy coding! 🚀