From Zero to AI

Lesson 3.1: Async/Await Syntax

Duration: 50 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  1. Define async functions using the async keyword
  2. Use await to pause execution until a Promise resolves
  3. Understand how async/await relates to Promises
  4. 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:

  1. They always return a Promise
  2. If you return a value, it is automatically wrapped in Promise.resolve()
  3. 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:

  1. The async function pauses at the await expression
  2. It waits for the Promise to settle (fulfill or reject)
  3. If fulfilled, await returns the resolved value
  4. If rejected, await throws the rejection reason
  5. 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

  1. async keyword marks a function as asynchronous and makes it return a Promise
  2. await keyword pauses execution until a Promise settles and returns its value
  3. Async/await makes asynchronous code read like synchronous code
  4. You can only use await inside an async function (or at top-level in ES modules)
  5. Any value returned from an async function is automatically wrapped in a Promise
  6. 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.