async/await is syntactic sugar on top of Promises. It lets you write async code that reads top-to-bottom, just like synchronous code. This is what you’ll use 90% of the time.
Adding async to a function does one thing: it makes the function always return a Promise.
Copy
async function greet() { return "Hello!";}// Equivalent to:function greet() { return Promise.resolve("Hello!");}// Both return a Promisegreet().then(message => console.log(message)); // "Hello!"
This means any function that uses await inside it must be marked async.
Copy
// ❌ Error: await is only valid in async functionsfunction getUsers() { const response = await fetch("/api/users"); // SyntaxError!}// ✅ Correct: mark function as asyncasync function getUsers() { const response = await fetch("/api/users"); return response.json();}
await pauses the function until the Promise settles. The function doesn’t block the page — it just pauses internally while other code keeps running.
Copy
async function fetchAndLog() { console.log("1. Starting fetch..."); const response = await fetch("/api/users"); // Pauses here console.log("2. Got response"); // Runs after fetch completes const users = await response.json(); // Pauses here console.log("3. Parsed JSON"); // Runs after parsing return users;}// Meanwhile, the rest of your app stays interactivefetchAndLog();console.log("4. This runs immediately — doesn't wait for fetchAndLog");// Output:// 1. Starting fetch...// 4. This runs immediately — doesn't wait for fetchAndLog// 2. Got response// 3. Parsed JSON
await pauses the current async function, not the entire program. Other code outside the function continues to run. That’s why line 4 prints before lines 2 and 3.
finally is perfect for resetting loading states. It runs regardless of success or failure, so you don’t need to set loading = false in both the try and catch blocks.
async function getUserOrders(userId) { const userResponse = await fetch(`/api/users/${userId}`); const user = await userResponse.json(); // Need the user first to get their orders const ordersResponse = await fetch(`/api/orders?userId=${user.id}`); const orders = await ordersResponse.json(); return { user, orders };}
When requests are independent, use Promise.all() with await:
Copy
async function getDashboardData() { // ❌ Sequential — slow (each waits for the previous) const users = await fetch("/api/users").then(r => r.json()); const products = await fetch("/api/products").then(r => r.json()); const orders = await fetch("/api/orders").then(r => r.json()); // Total time: request1 + request2 + request3 // ✅ Parallel — fast (all run at the same time) const [users, products, orders] = await Promise.all([ fetch("/api/users").then(r => r.json()), fetch("/api/products").then(r => r.json()), fetch("/api/orders").then(r => r.json()), ]); // Total time: max(request1, request2, request3)}
A common mistake is using await for every request even when they don’t depend on each other. If two requests are independent, run them in parallel with Promise.all().
async function getUser(userId) { try { const response = await fetch(`/api/users/${userId}`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { console.error("Error:", error.message); throw error; }}// Call itconst user = await getUser(1);
Copy
import httpxasync def get_user(user_id: int): try: async with httpx.AsyncClient() as client: response = await client.get(f"/api/users/{user_id}") response.raise_for_status() return response.json() except httpx.HTTPError as error: print(f"Error: {error}") raise# Call ituser = await get_user(1)
The syntax is nearly identical. The big difference: in Python, async is opt-in (most code is synchronous). In JavaScript, any code that touches the network is async by default.