Skip to main content

What is Async/Await?

Async/await is a modern way to handle asynchronous operations (like API calls) that makes your code look synchronous and easier to read.
  • async → Marks a function as asynchronous (it can wait for things)
  • await → “Wait here until this Promise resolves”

What is a Promise?

A Promise is like a “ticket” for a future value. Think of ordering food:
  1. You order → You get a receipt (Promise)
  2. You wait → The promise is “pending”
  3. Food arrives → The promise is “resolved” with your food
Or the kitchen runs out → The promise is “rejected” with an error

Basic Syntax

async function functionName(): Promise<ReturnType> {
  const result = await asynchronousOperation();
  return result;
}

Real Example: Fetching Products

From our project, here’s how we fetch products from an API:
main.ts
// async function → This function is asynchronous
// Promise<Product[]> → It promises to return an array of Products
// limit: number = 20 → Parameter with default value
async function fetchProducts(limit: number = 20): Promise<Product[]> {
  // Template literal: build the URL with the limit
  const url = `${API_URL}?limit=${limit}`;
  console.log(`Fetching: ${url}`);
  
  // await fetch(url) → Wait for the server to respond
  // response is of type Response (object with metadata + methods)
  const response = await fetch(url);
  
  // Check if response was successful (status 200-299)
  if (!response.ok) {
    // If not successful, throw an error with HTTP code
    throw new Error(`HTTP Error: ${response.status}`);
  }
  
  // await response.json() → Wait and parse the JSON
  // as Product[] → Tell TypeScript the JSON is a Product array
  const data = await response.json() as Product[];
  console.log(`Products received: ${data.length}`);
  
  // Return the products
  return data;
}

The Fetch Flow

Your App                API Server
  │                         │
  │──── fetch(url) ────────>│  1. Send request
  │      (await)            │  2. Wait...
  │<──── Response ──────────│  3. Receive response
  │                         │
  │── response.json() ─────>│  4. Parse JSON
  │      (await)            │  5. Wait...
  │<──── Data[] ────────────│  6. We have the data!

Without Async/Await (Old Way)

Before async/await, we used .then() chains:
// ❌ Hard to read
function fetchProducts(limit: number): Promise<Product[]> {
  return fetch(`${API_URL}?limit=${limit}`)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP Error: ${response.status}`);
      }
      return response.json();
    })
    .then(data => {
      console.log(`Products received: ${data.length}`);
      return data;
    });
}

With Async/Await (Modern Way)

// ✅ Much easier to read!
async function fetchProducts(limit: number): Promise<Product[]> {
  const response = await fetch(`${API_URL}?limit=${limit}`);
  
  if (!response.ok) {
    throw new Error(`HTTP Error: ${response.status}`);
  }
  
  const data = await response.json();
  console.log(`Products received: ${data.length}`);
  return data;
}

Error Handling with Try/Catch

Use try/catch to handle errors gracefully:
main.ts
async function loadProducts(): Promise<void> {
  // 1. Update state to "loading"
  appState.status = LoadingState.Loading;
  appState.error = null;  // Clear any previous error
  updateUI();  // Reflect change in UI (shows spinner)
  
  // 2. Try to load products (might fail)
  try {
    // await waits for fetchProducts to finish
    const products = await fetchProducts(20);
    
    // If we get here, loading was successful
    appState.status = LoadingState.Success;
    appState.products = products;
    
  } catch (error) {
    // If something fails (network, server, etc.), we arrive here
    
    // Check if 'error' is an Error instance to access .message
    // If not Error (could be a string or something else), use generic message
    appState.error = error instanceof Error ? error.message : "Unknown error";
    appState.status = LoadingState.Error;
    
    // Log error to console for debugging
    console.error("Error loading products:", error);
  }
  
  // 3. Update UI (whether success or error)
  updateUI();
}

When to Use Async/Await

Use async/await for operations that take time:
  • API calls (fetch, axios)
  • Database queries
  • File operations
  • Timers (setTimeout as Promise)
  • Any function that returns a Promise

Multiple Awaits (Sequential)

When you need things to happen in order:
async function processOrder() {
  const user = await fetchUser();        // Wait for user
  const cart = await fetchCart(user.id); // Then get cart (needs user.id)
  const order = await createOrder(cart); // Then create order (needs cart)
  return order;
}

Multiple Awaits (Parallel)

When things can happen at the same time:
async function loadPageData() {
  // These don't depend on each other - run them in parallel!
  const [products, categories, user] = await Promise.all([
    fetchProducts(),
    fetchCategories(),
    fetchUser()
  ]);
  
  return { products, categories, user };
}

Return Type: Promise<T>

Async functions always return a Promise:
async function fetchProducts(): Promise<Product[]> {
  // Even though we return Product[], the function returns Promise<Product[]>
  return data;
}

// To use it:
const products = await fetchProducts(); // products is Product[]

Async Functions Can’t Run in Regular Functions

// ❌ Error: 'await' is only valid in async functions
function loadData() {
  const data = await fetchProducts(); // Error!
}

// ✅ Correct: Mark the function as async
async function loadData() {
  const data = await fetchProducts(); // Works!
}

Common Pattern: Loading State Management

Combine async/await with enums for clean state management:
async function loadData(): Promise<void> {
  // Start loading
  setState(LoadingState.Loading);
  
  try {
    // Fetch data
    const data = await fetchData();
    
    // Success
    setState(LoadingState.Success);
    setData(data);
    
  } catch (error) {
    // Error
    setState(LoadingState.Error);
    setError(error.message);
  }
}

Async/Await with Event Listeners

You can use async functions with event listeners:
main.ts
function setupEventListeners(): void {
  const loadBtn = getElement<HTMLButtonElement>("#load-products-btn");
  
  // loadProducts is async - that's fine!
  loadBtn.addEventListener('click', loadProducts);
}
When the button is clicked, loadProducts runs asynchronously.

Error Types and Instanceof

Check error types safely:
main.ts
try {
  const data = await fetchProducts();
} catch (error) {
  // error could be anything (Error, string, number, etc.)
  // Check if it's an Error instance to access .message
  if (error instanceof Error) {
    console.error(error.message);  // Safe to access .message
  } else {
    console.error("Unknown error:", error);
  }
}

HTTP Status Codes

Common codes you’ll encounter:
CodeMeaning
200OK (success)
201Created (resource created successfully)
400Bad Request (invalid data sent)
401Unauthorized (need to log in)
404Not Found (resource doesn’t exist)
500Internal Server Error (server problem)
main.ts
const response = await fetch(url);

if (!response.ok) {
  // response.status has the HTTP code (404, 500, etc.)
  throw new Error(`HTTP Error: ${response.status}`);
}

Default Parameters

Provide default values for parameters:
main.ts
// limit: number = 20 → If no limit provided, use 20
async function fetchProducts(limit: number = 20): Promise<Product[]> {
  const url = `${API_URL}?limit=${limit}`;
  // ...
}

// Usage:
await fetchProducts();    // Uses default: 20
await fetchProducts(10);  // Uses provided: 10

Next Steps

Build docs developers (and LLMs) love