Lesson 3.1: Async/Await Syntax
Duration: 50 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Define async functions using the
asynckeyword - Use
awaitto pause execution until a Promise resolves - Understand how async/await relates to Promises
- Convert Promise chains to async/await syntax
Introduction
In the previous module, you learned how Promises provide a cleaner way to handle asynchronous operations compared to callbacks. But Promise chains can still become complex when you have multiple sequential operations. Async/await syntax, introduced in ES2017, makes asynchronous code look and behave like synchronous code.
Think of it this way: with Promises, you are giving instructions like "when this finishes, do that." With async/await, you are saying "wait here until this finishes, then continue."
The async Keyword
The async keyword is placed before a function declaration to mark it as an asynchronous function.
// Regular function
function getNumber(): number {
return 42;
}
// Async function
async function getNumberAsync(): Promise<number> {
return 42;
}
Key characteristics of async functions:
- They always return a Promise
- If you return a value, it is automatically wrapped in
Promise.resolve() - If you throw an error, it is wrapped in
Promise.reject()
async function greet(name: string): Promise<string> {
return `Hello, ${name}!`;
}
// Calling an async function
greet('Alice').then((message) => console.log(message));
// Output: Hello, Alice!
Even though we return a plain string, the function returns Promise<string>.
The await Keyword
The await keyword can only be used inside an async function. It pauses the execution of the function until the Promise settles.
async function fetchUserName(userId: number): Promise<string> {
// Simulating an API call
const response = await fetch(`https://api.example.com/users/${userId}`);
const user = await response.json();
return user.name;
}
What happens when you use await:
- The async function pauses at the
awaitexpression - It waits for the Promise to settle (fulfill or reject)
- If fulfilled,
awaitreturns the resolved value - If rejected,
awaitthrows the rejection reason - The function resumes execution after the await
Converting Promise Chains to Async/Await
Let us see how the same logic looks with Promises versus async/await.
Promise Chain Version
function getUserPosts(userId: number): Promise<Post[]> {
return fetch(`https://api.example.com/users/${userId}`)
.then((response) => response.json())
.then((user) => fetch(`https://api.example.com/posts?userId=${user.id}`))
.then((response) => response.json());
}
Async/Await Version
async function getUserPosts(userId: number): Promise<Post[]> {
const userResponse = await fetch(`https://api.example.com/users/${userId}`);
const user = await userResponse.json();
const postsResponse = await fetch(`https://api.example.com/posts?userId=${user.id}`);
const posts = await postsResponse.json();
return posts;
}
The async/await version:
- Reads from top to bottom like synchronous code
- Makes intermediate values easy to access
- Clearly shows the sequence of operations
Practical Example: Loading User Data
Let us build a function that loads a user and their related data.
interface User {
id: number;
name: string;
email: string;
}
interface Post {
id: number;
userId: number;
title: string;
body: string;
}
// Simulate API delays
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Simulate fetching a user
async function fetchUser(userId: number): Promise<User> {
await delay(500); // Simulate network delay
return {
id: userId,
name: 'John Doe',
email: 'john@example.com',
};
}
// Simulate fetching posts
async function fetchPosts(userId: number): Promise<Post[]> {
await delay(300);
return [
{ id: 1, userId, title: 'First Post', body: 'Hello World' },
{ id: 2, userId, title: 'Second Post', body: 'Learning async/await' },
];
}
// Main function combining both
async function loadUserWithPosts(userId: number): Promise<{ user: User; posts: Post[] }> {
console.log('Starting to load user data...');
const user = await fetchUser(userId);
console.log(`Loaded user: ${user.name}`);
const posts = await fetchPosts(user.id);
console.log(`Loaded ${posts.length} posts`);
return { user, posts };
}
// Usage
loadUserWithPosts(1).then((result) => {
console.log('Complete result:', result);
});
Awaiting Non-Promise Values
You can use await with non-Promise values. They are simply returned immediately without pausing.
async function example(): Promise<number> {
const value = await 42; // Works, but unnecessary
return value;
}
This is valid but pointless. Use await only with Promises.
Top-Level Await
In modern JavaScript (ES2022+) and TypeScript with ES module configuration, you can use await at the top level of a module without wrapping it in an async function.
// In an ES module file
const response = await fetch('https://api.example.com/config');
const config = await response.json();
export default config;
For this to work, your tsconfig.json needs:
{
"compilerOptions": {
"module": "ESNext",
"target": "ES2022"
}
}
Common Patterns
Sequential Operations
When operations depend on each other, use sequential awaits:
async function sequential(): Promise<void> {
const first = await step1(); // Wait for step1
const second = await step2(first); // Then step2
const third = await step3(second); // Then step3
console.log(third);
}
Storing Promises for Later
You can create Promises without awaiting them immediately:
async function example(): Promise<void> {
// Start the operation
const promise = fetchData();
// Do other things
console.log('Fetching started...');
// Now wait for the result
const data = await promise;
console.log('Data received:', data);
}
Exercise
Convert the following Promise chain to async/await syntax:
function getWeatherReport(city: string): Promise<string> {
return fetchCity(city)
.then((cityData) => fetchWeather(cityData.id))
.then((weather) => fetchForecast(weather.locationId))
.then((forecast) => {
return `Weather in ${city}: ${forecast.summary}`;
});
}
Solution:
async function getWeatherReport(city: string): Promise<string> {
const cityData = await fetchCity(city);
const weather = await fetchWeather(cityData.id);
const forecast = await fetchForecast(weather.locationId);
return `Weather in ${city}: ${forecast.summary}`;
}
Key Takeaways
- async keyword marks a function as asynchronous and makes it return a Promise
- await keyword pauses execution until a Promise settles and returns its value
- Async/await makes asynchronous code read like synchronous code
- You can only use
awaitinside an async function (or at top-level in ES modules) - Any value returned from an async function is automatically wrapped in a Promise
- Async/await is syntactic sugar over Promises - it does not replace them
Resources
| Resource | Type | Level |
|---|---|---|
| MDN: async function | Documentation | Beginner |
| MDN: await | Documentation | Beginner |
| JavaScript.info: Async/Await | Tutorial | Beginner |
| TypeScript Handbook: Everyday Types | Documentation | Intermediate |
Next Lesson
Continue to Lesson 3.2: Error Handling with try/catch to learn how to handle errors gracefully in async code.