JavaScript Async Await
Table of Contents + −
In the previous lesson, we learned how promises represent a value that arrives later and how .then() reads that value. Now let’s learn async/await, a cleaner way to work with the same promises that reads like ordinary top-to-bottom code.
⚡ What are Async and Await?
async/await is built on top of promises. It does not replace them; it gives you a nicer way to write them. The async keyword marks a function as asynchronous, and the await keyword waits for a promise inside that function.
| Keyword | Meaning | Where it goes |
async | Marks a function as asynchronous and makes it return a promise | Before the function keyword |
await | Pauses until a promise resolves, then gives you its value | Inside an async function |
An async function always returns a promise, even if you return a plain value:
async function getName() { return "Alex";}
getName().then((name) => console.log(name)); // AlexLet’s walk through what each line does:
async function getName()marksgetNameas asynchronous, so its return value is automatically wrapped in a promise.return "Alex"returns a plain string, but because the function isasync, the string comes back as a resolved promise that holds"Alex".getName().then((name) => ...)calls the function and uses.then()on the returned promise to read the value once it resolves, loggingAlex.
The string "Alex" is automatically wrapped in a resolved promise, so you can call .then() on the result.
⏳ The await Keyword
Inside an async function, await pauses the function until the promise settles and then hands you the resolved value. The rest of the function waits for that line to finish before it runs.
async function showUser() { const user = await fetchUser(); // pause here until the promise resolves console.log(user.name); // runs only after the value arrives}Here is how the function runs line by line:
await fetchUser()callsfetchUser, which returns a promise, and pauses the function until that promise resolves.const user = ...stores the resolved value (not the promise) once the pause ends.console.log(user.name)runs only after the value arrives, souseris ready to use.
The variable user holds the resolved value, not the promise itself. This is what makes await feel like synchronous code: each line waits for the one before it.
await needs async
You can only use await inside a function declared with async. Using it
anywhere else is a syntax error.
🔁 The Same Code: .then() vs async/await
The two snippets below do exactly the same thing: fetch a user, then print the name. The first uses .then() chaining, and the second uses async/await.
With .then(), each step lives inside a callback:
function showUser() { fetchUser().then((user) => { console.log(user.name); });}Let’s trace this version:
fetchUser()starts the work and returns a promise..then((user) => {...})registers a callback that runs later, once the promise resolves.console.log(user.name)runs inside that callback, withuserbeing the resolved value.
With async/await, the same logic reads top to bottom:
async function showUser() { const user = await fetchUser(); console.log(user.name);}Here is how the two versions map to each other:
await fetchUser()replaces the.then()call. Instead of passing a callback,awaitpauses and returns the resolved value directly.const user = ...plays the role of theuserparameter in the.then()callback. Both hold the same resolved value.console.log(user.name)is the same line as inside the callback, but now it sits on the next line instead of being nested.
Both call fetchUser(), wait for the result, and print the name. The async/await version avoids the nested callback and reads in the same order it runs.
🛡️ Handling Errors with try/catch
A promise can reject when something goes wrong. With async/await, you catch a rejected promise using a normal try/catch block, the same way you handle other errors in JavaScript.
async function showUser() { try { const user = await fetchUser(); // ✅ if this rejects, control jumps to catch console.log(user.name); } catch (error) { console.log("Something went wrong:", error.message); }}Let’s follow both paths through this code:
try { ... }wraps the lines that might fail so a rejection can be caught.await fetchUser()either resolves and lets the next line run, or rejects and throws an error.console.log(user.name)runs only when the promise resolves successfully.catch (error) { ... }runs instead when the promise rejects, anderror.messagedescribes what went wrong.
If the awaited promise rejects, the await line throws and the catch block runs. We will go deeper into error handling in the next-next lesson on error handling.
Always wrap risky awaits
Any await that can reject belongs inside try/catch. Without it, a
rejected promise becomes an unhandled error.
🧪 A Practical Example
Here we wait for a delayed promise and use its result. The delay function returns a promise that resolves after a set time, and loadMessage awaits it.
function delay(ms, value) { return new Promise((resolve) => { setTimeout(() => resolve(value), ms); });}
async function loadMessage() { console.log("Loading..."); const message = await delay(1000, "Data ready!"); // wait one second console.log(message); // Data ready!}
loadMessage();Let’s step through the example from top to bottom:
delay(ms, value)returns a new promise and usessetTimeoutto callresolve(value)aftermsmilliseconds, so the promise resolves withvalueonce the time passes.console.log("Loading...")runs first and printsLoading...right away.await delay(1000, "Data ready!")pausesloadMessagefor one second, then stores the resolved value"Data ready!"inmessage.console.log(message)printsData ready!after the pause ends.loadMessage()calls the function to start the whole sequence.
When loadMessage runs, it prints Loading..., pauses one second at the await line, and then prints Data ready!. The value passed to resolve becomes the value of the await expression.
⚠️ Common Mistakes to Avoid
| Mistake | Problem | Solution |
Using await outside an async function | JavaScript throws a syntax error | Put the await inside a function marked async |
Forgetting await | You get a Promise object instead of the value | Add await before the call that returns a promise |
Not wrapping await in try/catch | A rejected promise becomes an unhandled error | Surround risky awaits with try/catch |
Forgetting await is the most common slip. Compare these two lines:
const user = fetchUser(); // ❌ user is a Promise, not the valueconst user = await fetchUser(); // ✅ user is the resolved valueThe difference comes down to one keyword:
- The first line skips
await, souserholds the pendingPromiseobject anduser.namewould beundefined. - The second line adds
await, so the function pauses anduserholds the resolved value, ready to use.
🔧 Try It Yourself!
- Write a
delayfunction that returns a promise resolving after one second. - Create an
asyncfunction that awaitsdelayand logs the result. - Rewrite a
.then()chain you have seen as anasync/awaitfunction. - Wrap an
awaitintry/catchand make the promise reject to see the catch run.
🧩 What You’ve Learned
- ✅
async/awaitis a cleaner way to work with promises that reads like synchronous code - ✅ An
asyncfunction always returns a promise - ✅
awaitpauses inside anasyncfunction until a promise resolves and gives its value - ✅ You can only use
awaitinside anasyncfunction - ✅
try/catchhandles a rejected promise inasync/awaitcode
🚀 What’s Next?
Now that you can write asynchronous code that reads cleanly, let’s use it to request real data from the web. Let’s continue to Fetch API.