Asynchronous JavaScript is everywhere: API requests, database operations, file handling, timers, and background tasks all depend on non-blocking execution. The challenge is writing asynchronous code that remains readable, predictable, and easy to debug as applications grow.
Promises solved many problems around asynchronous workflows, but deeply chained .then() calls can still become difficult to follow. async and await provide a cleaner syntax that lets asynchronous code read almost like synchronous code while preserving JavaScript's non-blocking behavior.
Mastering async/await is not just about knowing the syntax. It means understanding promise behavior, error handling, parallel execution, sequential workflows, and when Promise.all() is a better choice.
Understanding Async Await in Real-World JavaScript
An async function always returns a Promise, even when it appears to return a normal value. The await keyword pauses execution inside that async function until the awaited Promise settles, without blocking the JavaScript event loop.
A common mistake is using await for independent operations that could run simultaneously. If two API requests do not depend on each other, starting both promises first and then awaiting them with Promise.all() can significantly reduce total waiting time.
Error handling is another important part of async/await. A try...catch block can handle rejected promises using familiar synchronous-looking control flow, while finally is useful for cleanup tasks that should happen regardless of success or failure.
The following example simulates a realistic order-processing workflow. It demonstrates sequential operations, parallel asynchronous tasks, structured error handling, artificial network delays, and detailed console output so you can observe exactly how the workflow progresses.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Simulate fetching a user from an API.
async function fetchUser(userId) {
console.log(`[1] Fetching user ${userId}...`);
await delay(700);
console.log(`[2] User ${userId} fetched successfully.`);
return {
id: userId,
name: "Ansh",
email: "ansh@example.com"
};
}
// Simulate fetching products independently from another service.
async function fetchProducts() {
console.log("[3] Fetching products...");
await delay(900);
console.log("[4] Products fetched successfully.");
return [
{ id: 101, name: "Keyboard", price: 2500 },
{ id: 102, name: "Mouse", price: 1200 },
{ id: 103, name: "Monitor", price: 15000 }
];
}
// Simulate fetching account preferences.
async function fetchPreferences(userId) {
console.log(`[5] Fetching preferences for user ${userId}...`);
await delay(600);
console.log("[6] Preferences fetched successfully.");
return {
currency: "INR",
notifications: true
};
}
// Simulate creating an order through an external service.
async function createOrder(user, product, preferences) {
console.log(`[7] Creating order for ${user.name}...`);
await delay(800);
const total = product.price;
console.log("[8] Order created successfully.");
return {
orderId: `ORD-${Date.now()}`,
customer: user.name,
product: product.name,
total,
currency: preferences.currency
};
}
// Main workflow demonstrates practical async/await patterns.
async function processOrder() {
console.log("--- Async/Await workflow started ---");
try {
// Step 1: Fetch the user first because later operations need user data.
const user = await fetchUser(42);
// These requests are independent, so start them together.
console.log("[9] Starting independent requests in parallel...");
const productsPromise = fetchProducts();
const preferencesPromise = fetchPreferences(user.id);
// Wait for both operations at the same time.
const [products, preferences] = await Promise.all([
productsPromise,
preferencesPromise
]);
console.log("[10] Parallel requests completed.");
// Select the first product for this demonstration.
const selectedProduct = products[0];
console.log(`[11] Selected product: ${selectedProduct.name}`);
// Create the order after all required data is available.
const order = await createOrder(
user,
selectedProduct,
preferences
);
console.log("[12] Final order details:");
console.log(JSON.stringify(order, null, 2));
console.log("--- Workflow completed successfully ---");
} catch (error) {
// Any rejected Promise inside the try block reaches this handler.
console.error("Order processing failed:", error.message);
} finally {
// Cleanup or logging can happen regardless of success or failure.
console.log("--- Async workflow finished ---");
}
}
// Execute the asynchronous workflow.
processOrder();
Conclusion
The key to mastering async/await is understanding what happens underneath the clean syntax. Promises still power the workflow, while async and await make asynchronous control flow easier to read and maintain.
For sequential dependencies, await keeps the code straightforward. For independent operations, start the promises together and use Promise.all() to avoid unnecessary waiting; combine that with try...catch for reliable error handling.
Once these patterns become familiar, async JavaScript becomes much easier to structure. The goal is not simply to replace .then() chains, but to design asynchronous workflows that are readable, efficient, resilient, and easy for other developers to maintain.
Top comments (0)