Your code runs in the wrong order. You call a function that loads data, and the very next line prints empty results, old values, or undefined. Nothing looks broken, yet the output is wrong. This is the most common confusion for JavaScript beginners, and it has a clear fix.
JavaScript does not wait for slow work by default. Reading a file, calling an API, or waiting for a timer takes time, and the rest of your code keeps running. In this guide you will learn how to make one line truly wait for an async function to finish before the next line runs.
Quick Answer
To wait for an async function to finish, mark your function with the async keyword and use await before the async call, like const data = await loadData();. The await keyword pauses that function until the promise resolves, so the next line only runs after the work is done. Remember that await only works inside an async function, or at the top level of an ES module.
Why JavaScript Does Not Wait by Default
JavaScript uses non-blocking behavior so the page does not freeze during slow work. When you start a slow task, JavaScript registers it, moves on, and comes back when it finishes.
This surprises new developers. You start fetching user data, then print the user on the next line. The print runs instantly, long before the reply arrives, so you see undefined or an empty object.
Async functions solve this by returning a special object called a promise. A promise is a placeholder for a value that will arrive later. The core job is learning how to pause until that placeholder becomes a real value. The concepts are explained clearly in the MDN Web Docs introduction to promises and asynchronous JavaScript, and every method below builds on them.
What an Async Function Really Returns
When you write the word async before a function, two things change. First, the function always returns a promise, even if you return a plain value. Second, you are allowed to use await inside it. If you forget this and treat the result like a normal value, you get a promise object instead of your data, which is another very common beginner bug.
Method 1: Use async and await (Recommended)
The clearest and most modern way to wait is async and await. It makes async code read like normal step-by-step code, which is why most tutorials and teams prefer it.
Follow these steps:
- Add the
asynckeyword to the function that needs to wait, for exampleasync function showUser() { ... }. - Inside that function, put
awaitdirectly before the async call, for exampleconst user = await getUser();. - Use the result on the following lines as a normal value, because execution only continues after the promise resolves.
- Call your async function normally from outside. Remember that the outer call itself also returns a promise.
Here is the shape of the code:
async function showUser() {
const user = await getUser();
console.log(user.name);
}
showUser();
The console.log line now runs after getUser() finishes, every time. Other code outside the function can still run during the pause, so your page stays responsive.
The syntax and rules are covered in depth in the MDN Web Docs reference for async functions and the await operator, including how errors behave like thrown exceptions.
Waiting for Several Steps in Order
Sometimes step two needs the result of step one. With await, this is simple. Write each call on its own line with its own await. The lines run strictly in order:
const user = await getUser();const posts = await getPosts(user.id);console.log(posts.length);
Each step waits for the previous one because each step depends on the last result. This is called sequential execution, and it is the right choice when the data depends on earlier data.
Method 2: Run Independent Tasks Together With Promise.all
Waiting in order is correct for dependent steps, but slow for independent ones. If you need a user profile, a product list, and store settings, and none of them depends on another, waiting one by one wastes time.
Use Promise.all() to start them together and wait for all of them:
- Start each async call without
await, so they begin running right away. - Pass the promises in an array to
Promise.all(). - Put
awaitbeforePromise.all()to pause until every promise finishes. - Read the results from the returned array in the same order as your input array.
In code, it looks like this:
const [user, products] = await Promise.all([getUser(), getProducts()]);
The total wait is only as long as the slowest single task. One warning: if any promise in the group rejects, Promise.all() rejects immediately. If you need every result even when some fail, look up Promise.allSettled() instead, which waits for all outcomes.
Method 3: Use .then() Chains
Before await existed, developers used the .then() method, and you will still see it in older code and some libraries. The idea is simple: “when this promise finishes, then run this function.”
The pattern is:
getUser().then((user) => { console.log(user.name); });
Code inside the .then() callback runs after the work completes. Code placed after the statement does not wait, which is the trap. Long chains are harder to read than await, and forgetting to return a promise inside a chain is a classic bug.
For new code, prefer async and await. Learn .then() mainly so you can read and maintain older projects with confidence.
How to Handle Errors While Waiting
Waiting is only half the job. Async work can fail. A network request can time out, and an API can return an error. With await, a rejected promise behaves like a thrown error, so you handle it with try...catch:
- Put your
awaitlines inside atryblock. - Add a
catchblock that receives the error. - Log the error and show the user a clear message or a fallback value.
- Optionally add a
finallyblock for cleanup, such as hiding a loading spinner.
Without this, a failed request can break your flow or leave a spinner spinning forever.
Special Cases You Will Meet
Top-Level await in Modules
Normally await must sit inside an async function. Modern ES modules are an exception: you can use await at the top level of a module file. This is handy in scripts and Node.js tools. If you try top-level await in a regular script file, you will get a syntax error, so check your file type first.
Event Handlers and Loops
Button click handlers and array methods cause frequent confusion. An event handler can be marked async directly, like button.addEventListener('click', async () => { await saveData(); }). Loops are trickier: forEach does not wait for async callbacks, so use a normal for...of loop with await when each item must finish before the next begins.
Waiting in a React or Frontend App
In frontend frameworks, you start async work inside an effect or handler and store the result in state. While debugging, you may also meet blockers covered in our guide on how to fix CORS errors when calling an API from localhost, which covers the other frequent reason local API calls never return data.
Common Mistakes and Useful Tips
- Forgetting await: the function returns a pending promise, and your next line uses the promise instead of the value. If you see
Promise { <pending> }in logs, a missingawaitis the cause. - Forgetting async: using
awaitinside a normal function is a syntax error. Addasyncto the function that contains theawait. - Assuming the outer code waits too: calling an async function does not pause the code that called it. If later code needs the result, it must also await the call or use
.then(). - Using await in forEach:
forEachignores the promises from async callbacks. Switch tofor...oforPromise.all()withmap(). - Awaiting everything one by one: independent tasks awaited in sequence are slow. Group them with
Promise.all()when order does not matter. - No error handling: always pair important
awaitcalls withtry...catch, or one network failure will break the flow.
Tip: add log lines before and after the async call. Their order in the console shows exactly what waited and what did not.
Conclusion
To make JavaScript wait, use await inside an async function. That single pattern covers most real work. Use Promise.all() when independent tasks can run together, add try...catch for failures, and remember that callers of an async function get a promise back. Once this mental model clicks, async code stops feeling random and starts feeling predictable.
Frequently Asked Questions
Can I use await outside an async function?
Only in ES modules, where top-level await is allowed. In regular scripts and normal functions, await must be inside a function marked with async. If you get a syntax error about await, check whether the containing function has the async keyword.
Why does my async function return a promise instead of the value?
Every async function automatically returns a promise. The value you return inside becomes the resolved value of that promise. Whoever calls your function must use await or .then() to get the real value out of the promise.
What is the difference between await and .then()?
Both wait for a promise to finish. await pauses the current function and reads like normal step-by-step code, while .then() registers a callback to run later and does not pause surrounding code. Most developers find await easier to read and debug.
How do I wait for multiple async functions at the same time?
Start all the calls first, then use await Promise.all([...]) with the promises in an array. Your code waits until the slowest one finishes, and you receive all results in one array in the same order you passed them in.
Why does my loop not wait for async code inside it?
Array methods like forEach do not pause between items, so all iterations start at once. Use a for...of loop with await inside it for strict one-by-one waiting, or collect the promises and await them together with Promise.all().


Leave a Reply