Have you ever wondered why, when you call a function that fetches data, you sometimes see await
in front of it, even if you know there's already an await
inside that function? It can feel a bit like waiting for someone to finish making coffee, only to realize you then have to wait again for them to pour it into your cup!
Let's clear up this common confusion and make async
and await
super easy to understand.
The Big Idea: Waiting in Code
Imagine you're building a house.
Synchronous Code: This is like building the house step-by-step. You can't put on the roof until the walls are completely finished. If the wall-building takes an hour, your roof-putting-on task has to sit there for an hour, doing nothing. In programming, if one part of your code takes a long time (like fetching data from the internet), it would make your entire program freeze! That's bad for user experience.
Asynchronous Code: This is like delegating tasks. You tell one team, "Go build the walls! I'll come back for the roof once you tell me you're done." While they're building the walls, you can go prepare the roof tiles or paint the windows. Your program doesn't freeze; it does other useful things.
Fetching data from an API (a server on the internet) is the classic example of an asynchronous task. It takes time, and you don't want your app to freeze while it's waiting for a response.
Promises: The "I'll Be Done Later" Note
Before async
and await
, we used Promises. A Promise is like an "I'll be done later" note. When you start an asynchronous task, you immediately get a Promise back. This Promise tells you:
"I'm pending." (I'm still working.)
"I'm fulfilled." (I finished successfully, and here's the result!)
"I'm rejected." (Something went wrong, and here's the error!)
So, when you call a function that performs an async task, it doesn't immediately give you the final data. It gives you a Promise that will eventually contain the data (or an error).
async
: Making Your Function Promise-Aware
The async
keyword is very simple:
You put
async
in front of a function declaration:async function fetchData() { ... }
This tells JavaScript: "This function will perform asynchronous operations."
Crucially: It also gives you the ability to use the
await
keyword inside this function.-
Most importantly: An
async
function always, always, always returns a Promise. No matter what youreturn
from anasync
function, JavaScript will automatically wrap that return value in aPromise
. If youthrow
an error inside anasync
function, JavaScript wraps that error in aPromise
that gets rejected.Think of it like a chef who promises you a dish. Even if they just stir a pot (a very quick step), they still hand you a "dish-in-progress" ticket when you ask for it.
await
: Patiently Waiting for the Promise
The await
keyword can only be used inside an async
function.
You put
await
in front of an expression that returns aPromise
:const data = await somePromise;
It tells JavaScript: "Pause the execution of this
async
function right here.""Wait until the
Promise
I'm waiting on (the one right afterawait
) either resolves (finishes successfully) or rejects (fails)."If the Promise resolves:
await
"unwraps" the Promise and gives you the actual value it resolved with. Your code then continues with that value.-
If the Promise rejects:
await
effectively "throws" that rejection as a regular JavaScript error. This means you can use a normaltry...catch
block around yourawait
calls to handle errors gracefully.This is you, the customer, patiently waiting at the counter with your "dish-in-progress" ticket until the chef actually hands you the finished dish.
The Big Question: Why await
Again? (The Double Wait)
This is the "aha!" moment for many. Let's use our chef analogy:
You have a "Dish Ordering" function (orderMeal
) and a "Chef's Cooking" function (cookDish
).
// The Chef's Kitchen (cookDish function)
async function cookDish(): Promise<string> {
console.log("Chef: Starting to cook...");
// Chef awaits ingredients delivery
const ingredients = await getIngredientsDelivery(); // AWAIT 1 (internal to chef)
console.log("Chef: Got ingredients!");
// Chef awaits cooking process
const cookedFood = await performCooking(ingredients); // AWAIT 2 (internal to chef)
console.log("Chef: Dish is ready!");
return "Delicious " + cookedFood; // Chef returns the finished dish.
// BUT since cookDish is 'async', it *actually* returns Promise<"Delicious ...">
}
// Your Dining Experience (orderMeal function)
async function orderMeal() {
console.log("Customer: Ordering meal...");
// You call the chef. What does cookDish() immediately give you?
// It gives you a Promise (a "Dish Promise Ticket")!
const mealPromise = cookDish();
console.log("Customer: Got a promise ticket:", mealPromise); // Prints Promise { <pending> }
// To get the *actual finished dish*, you need to await that promise ticket.
const actualDish = await mealPromise; // AWAIT 3 (external to chef, you await their final promise)
console.log("Customer: Received the actual dish:", actualDish);
}
// Kick it off
orderMeal();
Explanation:
Internal
await
(AWAIT 1 & 2): Theawait
s insidecookDish
are for the chef's own internal steps (getting ingredients, cooking). The chef needs to wait for these sub-tasks to finish before they can complete their overall job.External
await
(AWAIT 3): Theawait
outsidecookDish
(inorderMeal
) is becausecookDish
itself is anasync
function, and therefore, it returns a Promise. To get the finalstring
value ("Delicious cooked food") out of that Promise, you, the caller, mustawait
it.
The async
keyword effectively wraps your function's result in a Promise. The await
keyword then unwraps a Promise to give you its value. This wrapping and unwrapping is why you see await
at different levels: one for the internal workings, and one for the function's overall promised result.
Real-World Example: Fetching User Data
Let's see this in a common scenario: fetching user data from an API.
// 1. The Service Layer (internal 'await's)
// This function fetches data from a server and returns a Promise of UserData.
type UserData = { id: string; name: string; email: string };
async function fetchUserFromAPI(userId: string): Promise<UserData> {
console.log(`[Service] Fetching data for user ${userId}...`);
try {
// Await the network request itself
const response = await fetch(`https://api.example.com/users/${userId}`); // AWAIT 1 (network call)
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// Await the parsing of the JSON response body
const data: UserData = await response.json(); // AWAIT 2 (JSON parsing)
console.log(`[Service] Data fetched for user ${userId}.`);
return data; // This will be wrapped in a Promise by 'async'
} catch (error: any) {
console.error(`[Service Error] Failed to fetch user ${userId}:`, error.message);
throw new Error(`Could not get user data for ${userId}.`); // Re-throwing to caller
}
}
// 2. The Application Layer (external 'await's)
// This function uses the service to fetch data and display it.
async function displayUserProfile(userId: string) {
console.log(`[App] Preparing to display profile for ${userId}...`);
try {
// Await the Promise returned by fetchUserFromAPI
const userProfile = await fetchUserFromAPI(userId); // AWAIT 3 (awaiting the service's promise)
console.log(`[App] Displaying profile for ${userProfile.name}:`);
console.log(`ID: ${userProfile.id}, Email: ${userProfile.email}`);
} catch (error: any) {
console.error(`[App Error] Failed to display profile:`, error.message);
// Show an error message to the user in the UI
}
console.log(`[App] Profile display process complete.`);
}
// --- Let's run it (in a real browser/Node.js environment) ---
// Note: These console logs might not appear in this specific chat window
// but would in your browser's console or Node.js terminal.
console.log("Starting application...");
// This line itself does NOT wait for displayUserProfile to finish.
// It just calls the async function, which returns a Promise.
displayUserProfile("1");
displayUserProfile("invalid-id-that-will-fail"); // Example of an ID that would cause an error
console.log("Application started. Waiting for profiles to load...");
// A simple way to simulate API response for this article without actual network calls:
// In a real scenario, you'd replace 'fetch' with a mock during testing.
// For this example to "run" here, assume 'fetch' resolves/rejects as shown in the console logs.
In the example above, fetchUserFromAPI
uses await
internally to handle its own async steps. But displayUserProfile
must also use await
when calling fetchUserFromAPI
because fetchUserFromAPI
is an async
function and thus always returns a Promise
.
Conclusion
The "repetitive" await
is actually a fundamental aspect of how async/await
works to make asynchronous code appear sequential.
async
functions return Promises.await
unwraps Promises.
By understanding this wrapping and unwrapping, you can confidently use async/await
to write clean, readable, and robust asynchronous code in your JavaScript and TypeScript applications!
This article is just one small piece of my journey in building robust and maintainable code. If you're passionate about clean architecture, functional programming, or practical utilities, I invite you to explore my Coding Remarks repository. Let's learn and build better software together!