Docs

README

9.3 Async/Await

Introduction

Async/await is syntactic sugar built on top of Promises, making asynchronous code look and behave more like synchronous code. It was introduced in ES2017 and has become the preferred way to handle async operations.

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│              EVOLUTION OF ASYNC JAVASCRIPT                  │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│                                                             │
│  Callbacks ─────► Promises ─────► Async/Await               │
│     (1995)          (2015)           (2017)                 │
│                                                             │
│  callback(err, data)   promise.then()   async/await         │
│  ā”œā”€ā”€ Nested            ā”œā”€ā”€ Chainable    ā”œā”€ā”€ Synchronous     │
│  ā”œā”€ā”€ Error handling    ā”œā”€ā”€ Better       │   style           │
│  │   repetitive        │   error flow   ā”œā”€ā”€ Try/catch       │
│  └── "Callback hell"   └── Still some   └── Most readable   │
│                            nesting                          │
│                                                             │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Basic Syntax

Async Functions

// Declaring an async function
async function fetchData() {
  return 'data'; // Automatically wrapped in Promise
}

// Arrow function version
const fetchData = async () => {
  return 'data';
};

// Method in object
const obj = {
  async getData() {
    return 'data';
  },
};

// Method in class
class DataService {
  async fetch() {
    return 'data';
  }
}

The await Keyword

async function example() {
  // await pauses execution until promise resolves
  const result = await somePromise;

  // Code continues after promise resolves
  console.log(result);
}
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│                    HOW AWAIT WORKS                          │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│                                                             │
│  async function example() {                                 │
│      console.log("1. Start");                               │
│                                                             │
│      const result = await fetch(url);  ◄── Pauses here      │
│      │                                                      │
│      │   ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”                       │
│      └──►│ Control returns to       │                       │
│          │ event loop while waiting │                       │
│          ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜                       │
│                                                             │
│      console.log("2. Got result");  ◄── Resumes when ready  │
│  }                                                          │
│                                                             │
│  console.log("3. After call");  ◄── Runs immediately        │
│                                                             │
│  Output: 1. Start, 3. After call, 2. Got result             │
│                                                             │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Comparing Approaches

Callbacks vs Promises vs Async/Await

// āŒ Callbacks - nested, repetitive error handling
getUser(id, (err, user) => {
  if (err) return handleError(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handleError(err);
    getDetails(orders[0].id, (err, details) => {
      if (err) return handleError(err);
      console.log(details);
    });
  });
});

// āœ… Promises - flat chain, single catch
getUser(id)
  .then((user) => getOrders(user.id))
  .then((orders) => getDetails(orders[0].id))
  .then((details) => console.log(details))
  .catch(handleError);

// āœ…āœ… Async/Await - reads like synchronous code
async function loadDetails(id) {
  try {
    const user = await getUser(id);
    const orders = await getOrders(user.id);
    const details = await getDetails(orders[0].id);
    console.log(details);
  } catch (error) {
    handleError(error);
  }
}

Async Function Return Values

Always Returns a Promise

// These are equivalent:
async function getValue() {
  return 42;
}

function getValue() {
  return Promise.resolve(42);
}

// Usage
getValue().then((v) => console.log(v)); // 42

Throwing Errors

// Throwing in async function = rejected promise
async function willFail() {
  throw new Error('Something went wrong');
}

// Same as:
function willFail() {
  return Promise.reject(new Error('Something went wrong'));
}

// Both can be caught the same way
willFail().catch((e) => console.error(e.message));

Error Handling

Try/Catch Blocks

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Failed to fetch:', error);
    throw error; // Re-throw to propagate
  }
}

Catching Specific Errors

async function processUser(id) {
  try {
    const user = await getUser(id);

    try {
      await validateUser(user);
    } catch (validationError) {
      console.log('Validation failed, using defaults');
      user.role = 'guest';
    }

    return await saveUser(user);
  } catch (error) {
    console.error('Process failed:', error);
  }
}

Error Handling Patterns

// Pattern 1: Try/catch
async function pattern1() {
  try {
    const result = await riskyOperation();
    return result;
  } catch (error) {
    return defaultValue;
  }
}

// Pattern 2: .catch() on await
async function pattern2() {
  const result = await riskyOperation().catch((e) => defaultValue);
  return result;
}

// Pattern 3: Tuple return [error, result]
async function safeAwait(promise) {
  try {
    const result = await promise;
    return [null, result];
  } catch (error) {
    return [error, null];
  }
}

async function pattern3() {
  const [error, result] = await safeAwait(riskyOperation());
  if (error) {
    return defaultValue;
  }
  return result;
}

Sequential vs Parallel

Sequential Execution

// Each await waits for the previous one
async function sequential() {
  const start = Date.now();

  const a = await delay(100, 'A'); // Wait 100ms
  const b = await delay(100, 'B'); // Wait another 100ms
  const c = await delay(100, 'C'); // Wait another 100ms

  console.log(`Total: ${Date.now() - start}ms`); // ~300ms
  return [a, b, c];
}

Parallel Execution

// Start all promises at once
async function parallel() {
  const start = Date.now();

  // Start all at once
  const promiseA = delay(100, 'A');
  const promiseB = delay(100, 'B');
  const promiseC = delay(100, 'C');

  // Wait for all
  const a = await promiseA;
  const b = await promiseB;
  const c = await promiseC;

  console.log(`Total: ${Date.now() - start}ms`); // ~100ms
  return [a, b, c];
}

// Or use Promise.all
async function parallelWithAll() {
  const [a, b, c] = await Promise.all([
    delay(100, 'A'),
    delay(100, 'B'),
    delay(100, 'C'),
  ]);
  return [a, b, c];
}
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│             SEQUENTIAL vs PARALLEL                          │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│                                                             │
│  SEQUENTIAL (one after another)                             │
│  ────────────────────────────────                           │
│  |─ Task A ─|─ Task B ─|─ Task C ─|                         │
│  0         100        200        300ms                      │
│                                                             │
│  PARALLEL (all at once)                                     │
│  ────────────────────────                                   │
│  |─ Task A ─|                                               │
│  |─ Task B ─|                                               │
│  |─ Task C ─|                                               │
│  0         100ms                                            │
│                                                             │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Loops and Iteration

For...of Loop (Sequential)

async function processSequentially(items) {
  const results = [];

  for (const item of items) {
    const result = await processItem(item);
    results.push(result);
  }

  return results;
}

forEach Does NOT Work

// āŒ WRONG - forEach doesn't wait for async callbacks
async function broken(items) {
  items.forEach(async (item) => {
    await processItem(item); // These run in parallel!
  });
  // Function returns immediately, not when processing is done
}

// āœ… Use for...of instead
async function correct(items) {
  for (const item of items) {
    await processItem(item);
  }
}

Parallel Processing with map

async function processParallel(items) {
  const promises = items.map((item) => processItem(item));
  return await Promise.all(promises);
}

Controlled Concurrency

async function processBatched(items, batchSize) {
  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;
}

Common Patterns

Retry Pattern

async function retry(fn, attempts = 3, delay = 1000) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === attempts - 1) throw error;
      await new Promise((r) => setTimeout(r, delay));
      console.log(`Retry ${i + 1}/${attempts - 1}`);
    }
  }
}

// Usage
const data = await retry(() => fetchData(url), 3, 1000);

Timeout Pattern

async function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Timeout')), ms);
  });
  return Promise.race([promise, timeout]);
}

// Usage
try {
  const data = await withTimeout(fetchData(), 5000);
} catch (error) {
  if (error.message === 'Timeout') {
    console.log('Request timed out');
  }
}

Polling Pattern

async function poll(fn, interval, maxAttempts) {
  for (let i = 0; i < maxAttempts; i++) {
    const result = await fn();
    if (result.ready) {
      return result;
    }
    await new Promise((r) => setTimeout(r, interval));
  }
  throw new Error('Polling timeout');
}

// Usage
const status = await poll(() => checkJobStatus(jobId), 1000, 30);

Debounce Async

function debounceAsync(fn, delay) {
  let timeoutId;
  let pendingPromise = null;

  return async function (...args) {
    clearTimeout(timeoutId);

    return new Promise((resolve) => {
      timeoutId = setTimeout(async () => {
        const result = await fn.apply(this, args);
        resolve(result);
      }, delay);
    });
  };
}

const debouncedSearch = debounceAsync(searchAPI, 300);

Top-Level Await

In ES Modules (ES2022)

// In .mjs file or type: "module" in package.json
const config = await fetchConfig();
const db = await connectDatabase(config);

export { db };

In Classic Scripts (Use IIFE)

// Without top-level await, use IIFE
(async () => {
  const data = await fetchData();
  console.log(data);
})();

Async Generators

Creating Async Generators

async function* asyncGenerator() {
  yield await fetch('/api/1');
  yield await fetch('/api/2');
  yield await fetch('/api/3');
}

// Consuming
for await (const response of asyncGenerator()) {
  console.log(await response.json());
}

Practical Example: Paginated API

async function* fetchPages(url) {
  let nextUrl = url;

  while (nextUrl) {
    const response = await fetch(nextUrl);
    const data = await response.json();
    yield data.items;
    nextUrl = data.nextPage;
  }
}

// Usage
for await (const page of fetchPages('/api/items')) {
  console.log('Got page:', page);
}

Best Practices

1. Always Handle Errors

// āœ… Good
async function fetchData() {
  try {
    return await apiCall();
  } catch (error) {
    logger.error(error);
    throw error;
  }
}

// āŒ Bad - unhandled rejections
async function fetchData() {
  return await apiCall();
}

2. Avoid Unnecessary await

// āŒ Unnecessary - adding extra microtask
async function getValue() {
  return await Promise.resolve(42);
}

// āœ… Better - return directly
async function getValue() {
  return Promise.resolve(42);
}

3. Use Promise.all for Independent Operations

// āŒ Slow - sequential
async function getData() {
  const users = await getUsers();
  const posts = await getPosts();
  return { users, posts };
}

// āœ… Fast - parallel
async function getData() {
  const [users, posts] = await Promise.all([getUsers(), getPosts()]);
  return { users, posts };
}

4. Be Careful with this

class Service {
  constructor() {
    this.value = 42;
  }

  // āœ… Method retains 'this'
  async getValue() {
    return this.value;
  }
}

const service = new Service();
const fn = service.getValue.bind(service); // Bind if passing around

Summary

ConceptDescription
asyncDeclares function returns Promise
awaitPauses until Promise resolves
Error handlingUse try/catch blocks
Sequentialawait one after another
ParallelStart all, await Promise.all
for...ofSequential async loop
forEachDoes NOT work with async
Top-level awaitES modules only
Async generatorsasync function* with for await

What's Next?

In the next section, we'll dive deeper into Error Handling patterns for async code, including:

  • •Centralized error handling
  • •Error boundaries
  • •Retry strategies
  • •Graceful degradation
README - JavaScript Tutorial | DeepML