NexusCS

Async/Await

JavaScript
Quick reference for JavaScript's async/await syntax for handling asynchronous operations with cleaner, more readable code than traditional Promise chains.
javascript
typescript
promises
async

Getting started

Introduction

Async/await is modern JavaScript syntax for working with Promises. It makes asynchronous code look and behave more like synchronous code.

Browser Support: Baseline Widely Available (April 2017+)

Basic Async Function

async function fetchUser() {
  const response = await fetch("/api/user");
  const data = await response.json();
  return data;
}

// Returns a Promise
fetchUser().then((user) => console.log(user));

async functions always return a Promise. await pauses execution until the Promise resolves.

Async Arrow Functions

const fetchUser = async () => {
  const response = await fetch("/api/user");
  return response.json();
};

// One-liner
const getUser = async (id) => await fetch(`/api/user/${id}`);

Arrow functions can be async too.

Quick Example

async function main() {
  try {
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);
    console.log(posts);
  } catch (error) {
    console.error("Error:", error);
  }
}

main();

Basic Syntax

Async Function Declaration

async function myFunction() {
  return "Hello";
}

// Equivalent to:
function myFunction() {
  return Promise.resolve("Hello");
}

Async Class Methods

class UserService {
  async getUser(id) {
    const response = await fetch(`/api/user/${id}`);
    return response.json();
  }

  async updateUser(id, data) {
    await fetch(`/api/user/${id}`, {
      method: "PUT",
      body: JSON.stringify(data),
    });
  }
}

Await Expression

// Wait for Promise to resolve
const result = await promise;

// Works with any thenable
const value = await Promise.resolve(42);

// Wait for function call
const data = await fetchData();

await only works inside async functions (or at top level in modules).

Top-Level Await (ES2022)

// In ES modules (.mjs or type: "module")
const response = await fetch("/api/config");
const config = await response.json();

export default config;

Use in module scope without wrapping in async function.

Error Handling

Try...Catch

async function fetchData() {
  try {
    const response = await fetch("/api/data");
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Fetch failed:", error);
    throw error; // Re-throw or handle
  }
}

Standard error handling pattern.

Multiple Try Blocks

async function processData() {
  let user, posts;

  try {
    user = await fetchUser();
  } catch (error) {
    console.error("User fetch failed:", error);
    return null;
  }

  try {
    posts = await fetchPosts(user.id);
  } catch (error) {
    console.error("Posts fetch failed:", error);
    posts = []; // Fallback
  }

  return { user, posts };
}

Handle errors at different levels.

Return vs Return Await

// ❌ Wrong: Error not caught
async function wrong() {
  try {
    return fetch("/api/data"); // Returns immediately
  } catch (error) {
    // Never reached if fetch fails
  }
}

// ✅ Correct: Error caught
async function correct() {
  try {
    return await fetch("/api/data"); // Waits for Promise
  } catch (error) {
    console.error(error); // Catches fetch errors
  }
}

Use return await in try blocks to catch errors.

Error Propagation

async function inner() {
  throw new Error("Inner error");
}

async function middle() {
  await inner(); // Error propagates up
}

async function outer() {
  try {
    await middle();
  } catch (error) {
    console.error("Caught:", error.message);
  }
}

Errors bubble up through async call stack.

Concurrency Patterns

Promise.all() - Parallel

async function fetchAll() {
  const [users, posts, comments] = await Promise.all([
    fetch("/api/users").then((r) => r.json()),
    fetch("/api/posts").then((r) => r.json()),
    fetch("/api/comments").then((r) => r.json()),
  ]);

  return { users, posts, comments };
}

Run multiple Promises in parallel. Fails fast if any Promise rejects.

Promise.allSettled()

async function fetchAllSafe() {
  const results = await Promise.allSettled([
    fetch("/api/users"),
    fetch("/api/posts"),
    fetch("/api/comments"),
  ]);

  results.forEach((result, index) => {
    if (result.status === "fulfilled") {
      console.log(`Request ${index} succeeded:`, result.value);
    } else {
      console.error(`Request ${index} failed:`, result.reason);
    }
  });
}

Wait for all Promises, regardless of success or failure.

Promise.race()

async function fetchWithTimeout(url, timeout = 5000) {
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Timeout")), timeout),
  );

  const response = await Promise.race([fetch(url), timeoutPromise]);

  return response;
}

Returns first settled Promise (success or failure).

Promise.any()

async function fetchFromMirrors() {
  const mirrors = [
    "https://cdn1.example.com/data",
    "https://cdn2.example.com/data",
    "https://cdn3.example.com/data",
  ];

  const response = await Promise.any(mirrors.map((url) => fetch(url)));

  return response.json();
}

Returns first successful Promise. Rejects only if all fail.

Sequential vs Parallel

// ❌ Sequential (slower): 3 seconds total
async function sequential() {
  const user = await fetchUser(); // 1s
  const posts = await fetchPosts(); // 1s
  const comments = await fetchComments(); // 1s
  return { user, posts, comments };
}

// ✅ Parallel (faster): 1 second total
async function parallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(), // All start immediately
    fetchPosts(),
    fetchComments(),
  ]);
  return { user, posts, comments };
}

Use parallel execution when operations are independent.

Controlled Parallelism

async function processBatch(items, batchSize = 3) {
  const results = [];

  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map((item) => processItem(item)),
    );
    results.push(...batchResults);
  }

  return results;
}

Process items in batches to limit concurrency.

Common Patterns

Async IIFE

// Execute async code immediately
(async () => {
  const data = await fetchData();
  console.log(data);
})();

// Or with error handling
(async () => {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
})();

Immediately Invoked Function Expression for top-level async code.

Fetching Data

async function fetchUserData(userId) {
  const response = await fetch(`/api/user/${userId}`);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return response.json();
}

// With headers
async function postData(url, data) {
  const response = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });

  return response.json();
}

Multiple Awaits

async function getUserPosts(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts);

  return {
    user,
    posts: posts.map((post) => ({
      ...post,
      comments: comments.filter((c) => c.postId === post.id),
    })),
  };
}

Chain dependent async operations.

Conditional Async

async function getData(useCache = true) {
  if (useCache) {
    const cached = await getFromCache();
    if (cached) return cached;
  }

  const fresh = await fetchFromAPI();

  if (useCache) {
    await saveToCache(fresh);
  }

  return fresh;
}

Retry Logic

async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fetch(url);
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
    }
  }
}

Retry failed operations with exponential backoff.

Async Array Methods

// Map with await
const users = await Promise.all(
  userIds.map(async (id) => {
    return await fetchUser(id);
  }),
);

// Filter with await
const activeUsers = await Promise.all(
  users.map(async (user) => ({
    user,
    isActive: await checkUserActive(user.id),
  })),
).then((results) => results.filter((r) => r.isActive).map((r) => r.user));

// Reduce with await
const total = await items.reduce(async (accPromise, item) => {
  const acc = await accPromise;
  const value = await getValue(item);
  return acc + value;
}, Promise.resolve(0));

TypeScript

Async Return Types

// Explicit Promise type
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/user/${id}`);
  return response.json();
}

// Type inference works too
async function getUser(id: string) {
  // Return type inferred as Promise<User>
  const user: User = await fetchUser(id);
  return user;
}

Promise Generic Types

// Promise with generic type
const promise: Promise<number> = Promise.resolve(42);

// Await infers type
const value: number = await promise;

// Array of Promises
const promises: Promise<string>[] = [
  fetch("/a").then((r) => r.text()),
  fetch("/b").then((r) => r.text()),
];

const results: string[] = await Promise.all(promises);

Async Function Types

// Function type
type AsyncFunc = () => Promise<void>;

const myFunc: AsyncFunc = async () => {
  await doSomething();
};

// Generic async function
type FetchFunc<T> = (id: string) => Promise<T>;

const fetchUser: FetchFunc<User> = async (id) => {
  const response = await fetch(`/api/user/${id}`);
  return response.json();
};

Type Guards with Async

interface User {
  id: string;
  name: string;
}

interface Admin extends User {
  role: 'admin';
}

async function isAdmin(user: User): Promise<user is Admin> {
  const roles = await fetchUserRoles(user.id);
  return roles.includes('admin');
}

// Usage
const user = await fetchUser('123');
if (await isAdmin(user)) {
  // user is typed as Admin here
  console.log(user.role);
}

Common Pitfalls

Floating Promises

// ❌ Wrong: Promise ignored
async function save() {
  saveToDatabase(data); // No await!
  return "Saved";
}

// ✅ Correct: Wait for completion
async function save() {
  await saveToDatabase(data);
  return "Saved";
}

// ✅ Or explicitly ignore
async function save() {
  void saveToDatabase(data); // Intentional
  return "Saved";
}

Always await Promises or explicitly void them.

Sequential Loops

// ❌ Slow: Sequential execution
async function processItems(items) {
  for (const item of items) {
    await processItem(item); // Waits for each
  }
}

// ✅ Fast: Parallel execution
async function processItems(items) {
  await Promise.all(items.map((item) => processItem(item)));
}

Don't await inside loops unless order matters.

forEach with Async

// ❌ Wrong: Doesn't wait
async function processAll(items) {
  items.forEach(async (item) => {
    await processItem(item); // forEach doesn't await
  });
  console.log("Done"); // Runs immediately!
}

// ✅ Correct: Use for...of
async function processAll(items) {
  for (const item of items) {
    await processItem(item);
  }
  console.log("Done"); // Runs after all complete
}

// ✅ Or Promise.all
async function processAll(items) {
  await Promise.all(items.map((item) => processItem(item)));
  console.log("Done");
}

forEach doesn't work with async/await.

Missing Return Await

// ❌ Wrong: Error escapes try block
async function getData() {
  try {
    return fetchData(); // Returns Promise immediately
  } catch (error) {
    // Never catches fetchData errors
  }
}

// ✅ Correct
async function getData() {
  try {
    return await fetchData();
  } catch (error) {
    console.error(error);
  }
}

Mixing Callbacks and Promises

// ❌ Wrong: Mixing paradigms
async function readFile(path) {
  fs.readFile(path, (err, data) => {
    if (err) throw err; // Doesn't work with async!
    return data;
  });
}

// ✅ Correct: Promisify or use Promise API
async function readFile(path) {
  return fs.promises.readFile(path);
}

// ✅ Or wrap callback
async function readFile(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

Best Practices

Always Handle Errors

// ✅ With try...catch
async function getData() {
  try {
    return await fetch("/api/data");
  } catch (error) {
    console.error("Fetch failed:", error);
    throw error;
  }
}

// ✅ Or with .catch()
async function getData() {
  return fetch("/api/data").catch((error) => {
    console.error("Fetch failed:", error);
    throw error;
  });
}

// ✅ Or at call site
getData().catch((error) => {
  console.error("Error:", error);
});

Prefer Parallel Execution

// ✅ Good: Parallel for independent operations
async function fetchData() {
  const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);
  return { users, posts };
}

// ✅ Good: Sequential for dependent operations
async function getUserPosts(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchPosts(user.id); // Needs user.id
  return posts;
}

Use Async/Await Over Chains

// ❌ Less readable
function getUserData(userId) {
  return fetchUser(userId)
    .then((user) => fetchPosts(user.id))
    .then((posts) => fetchComments(posts))
    .then((comments) => ({ posts, comments }))
    .catch((error) => console.error(error));
}

// ✅ More readable
async function getUserData(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts);
    return { posts, comments };
  } catch (error) {
    console.error(error);
  }
}

Handle Errors at Appropriate Level

// ✅ Low-level function throws
async function fetchUser(id) {
  const response = await fetch(`/api/user/${id}`);
  if (!response.ok) throw new Error("Fetch failed");
  return response.json();
}

// ✅ High-level function handles
async function displayUser(id) {
  try {
    const user = await fetchUser(id);
    renderUser(user);
  } catch (error) {
    showError("Could not load user");
    logError(error);
  }
}

Avoid Memory Leaks

// ✅ Clean up in finally
async function processData() {
  const connection = await openConnection();
  try {
    return await connection.query("SELECT * FROM users");
  } finally {
    await connection.close(); // Always runs
  }
}

// ✅ Use AbortController for fetch
async function fetchWithCancel(url, signal) {
  try {
    const response = await fetch(url, { signal });
    return response.json();
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("Fetch cancelled");
    }
    throw error;
  }
}

Document Async Dependencies

/**
 * Fetches user data and their posts
 * @param userId - User ID to fetch
 * @returns Promise resolving to user with posts
 * @throws {Error} If user not found or network fails
 */
async function getUserWithPosts(userId: string): Promise<UserWithPosts> {
  const user = await fetchUser(userId);
  const posts = await fetchPosts(user.id);
  return { ...user, posts };
}

Examples

REST API Client

class APIClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const response = await fetch(url, {
      ...options,
      headers: {
        "Content-Type": "application/json",
        ...options.headers,
      },
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return response.json();
  }

  async get(endpoint) {
    return this.request(endpoint);
  }

  async post(endpoint, data) {
    return this.request(endpoint, {
      method: "POST",
      body: JSON.stringify(data),
    });
  }

  async put(endpoint, data) {
    return this.request(endpoint, {
      method: "PUT",
      body: JSON.stringify(data),
    });
  }

  async delete(endpoint) {
    return this.request(endpoint, {
      method: "DELETE",
    });
  }
}

// Usage
const client = new APIClient("https://api.example.com");
const users = await client.get("/users");
await client.post("/users", { name: "John" });

Rate Limiter

class RateLimiter {
  constructor(maxConcurrent = 3) {
    this.maxConcurrent = maxConcurrent;
    this.running = 0;
    this.queue = [];
  }

  async run(fn) {
    while (this.running >= this.maxConcurrent) {
      await new Promise((resolve) => this.queue.push(resolve));
    }

    this.running++;
    try {
      return await fn();
    } finally {
      this.running--;
      const resolve = this.queue.shift();
      if (resolve) resolve();
    }
  }
}

// Usage
const limiter = new RateLimiter(3);

const urls = [...Array(10)].map((_, i) => `/api/data/${i}`);
const results = await Promise.all(
  urls.map((url) => limiter.run(() => fetch(url))),
);

Cache with TTL

class AsyncCache {
  constructor(ttl = 60000) {
    this.cache = new Map();
    this.ttl = ttl;
  }

  async get(key, fetcher) {
    const cached = this.cache.get(key);

    if (cached && Date.now() - cached.timestamp < this.ttl) {
      return cached.value;
    }

    const value = await fetcher();
    this.cache.set(key, {
      value,
      timestamp: Date.now(),
    });

    return value;
  }

  clear() {
    this.cache.clear();
  }
}

// Usage
const cache = new AsyncCache(30000); // 30 second TTL

async function getUser(id) {
  return cache.get(`user:${id}`, () =>
    fetch(`/api/user/${id}`).then((r) => r.json()),
  );
}

Async Queue

class AsyncQueue {
  constructor() {
    this.queue = [];
    this.processing = false;
  }

  async add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.process();
    });
  }

  async process() {
    if (this.processing || this.queue.length === 0) {
      return;
    }

    this.processing = true;

    while (this.queue.length > 0) {
      const { task, resolve, reject } = this.queue.shift();
      try {
        const result = await task();
        resolve(result);
      } catch (error) {
        reject(error);
      }
    }

    this.processing = false;
  }
}

// Usage
const queue = new AsyncQueue();

queue.add(() => processItem(1));
queue.add(() => processItem(2));
queue.add(() => processItem(3));

Parallel Limit Map

async function mapWithLimit(items, limit, asyncFn) {
  const results = [];
  const executing = [];

  for (const item of items) {
    const promise = asyncFn(item).then((result) => {
      executing.splice(executing.indexOf(promise), 1);
      return result;
    });

    results.push(promise);
    executing.push(promise);

    if (executing.length >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

// Usage
const urls = [...Array(100)].map((_, i) => `/api/item/${i}`);
const results = await mapWithLimit(
  urls,
  5, // Max 5 concurrent requests
  (url) => fetch(url).then((r) => r.json()),
);

Also see