Async JavaScriptFeatured

JavaScript Async/Await: Complete Guide to Asynchronous Programming

Master async/await in JavaScript. Learn promises, async functions, error handling, parallel execution, and best practices for asynchronous code.

By JavaScriptDoc Team
asyncawaitpromisesasynchronousjavascripterror handling

JavaScript Async/Await: Complete Guide to Asynchronous Programming

Async/await is a modern way to handle asynchronous operations in JavaScript. Built on top of Promises, it makes asynchronous code look and behave like synchronous code, making it easier to read and maintain.

Understanding Asynchronous JavaScript

Before diving into async/await, let's understand why we need asynchronous programming:

// Synchronous code - blocks execution
console.log('Start');
const data = fetchDataSync(); // This would block
console.log(data);
console.log('End');

// Asynchronous code - non-blocking
console.log('Start');
fetchDataAsync().then((data) => {
  console.log(data);
});
console.log('End'); // This runs before data is fetched

Promises: The Foundation

Async/await is built on Promises, so understanding them is crucial:

// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('Operation successful!');
    } else {
      reject('Operation failed!');
    }
  }, 1000);
});

// Using a Promise
myPromise
  .then((result) => console.log(result))
  .catch((error) => console.error(error));

// Promise states:
// 1. Pending - initial state
// 2. Fulfilled - operation completed successfully
// 3. Rejected - operation failed

Introduction to Async/Await

Async/await provides a cleaner syntax for working with Promises:

// Promise approach
function fetchUser() {
  return fetch('/api/user')
    .then((response) => response.json())
    .then((data) => {
      console.log(data);
      return data;
    })
    .catch((error) => {
      console.error('Error:', error);
    });
}

// Async/await approach
async function fetchUser() {
  try {
    const response = await fetch('/api/user');
    const data = await response.json();
    console.log(data);
    return data;
  } catch (error) {
    console.error('Error:', error);
  }
}

Async Functions

An async function always returns a Promise:

// Basic async function
async function greet() {
  return 'Hello, World!';
}

// Equivalent to:
function greet() {
  return Promise.resolve('Hello, World!');
}

// Using the function
greet().then((message) => console.log(message)); // 'Hello, World!'

// Async arrow functions
const greetArrow = async () => 'Hello, World!';

// Async function expressions
const greetExpr = async function () {
  return 'Hello, World!';
};

// Async methods in objects
const obj = {
  async method() {
    return 'Hello from method!';
  },
};

// Async methods in classes
class Greeter {
  async greet() {
    return 'Hello from class!';
  }
}

The Await Keyword

await pauses the execution of an async function until the Promise is resolved:

async function fetchData() {
  console.log('Fetching data...');

  // await pauses here until fetch completes
  const response = await fetch('/api/data');
  console.log('Got response');

  // await pauses again for json parsing
  const data = await response.json();
  console.log('Parsed data:', data);

  return data;
}

// await can only be used inside async functions
// This would cause an error:
// const data = await fetch('/api/data'); // SyntaxError

// But top-level await is supported in modules:
// In a .mjs file or <script type="module">
const response = await fetch('/api/data');

Error Handling

Try-Catch Blocks

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

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

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Failed to fetch user:', error);
    // Re-throw to let caller handle it
    throw error;
  }
}

// Using the function
async function displayUser(userId) {
  try {
    const user = await fetchUserData(userId);
    console.log('User:', user);
  } catch (error) {
    console.log('Could not display user');
  }
}

Error Handling Patterns

// Pattern 1: Individual try-catch
async function pattern1() {
  let user, posts;

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

  try {
    posts = await fetchPosts();
  } catch (error) {
    console.error('Posts fetch failed:', error);
    posts = [];
  }

  return { user, posts };
}

// Pattern 2: Catch method
async function pattern2() {
  const user = await fetchUser().catch((err) => {
    console.error('User fetch failed:', err);
    return null;
  });

  const posts = await fetchPosts().catch((err) => {
    console.error('Posts fetch failed:', err);
    return [];
  });

  return { user, posts };
}

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

async function pattern3() {
  const [user, userError] = await safeAwait(fetchUser());
  const [posts, postsError] = await safeAwait(fetchPosts());

  if (userError) console.error('User error:', userError);
  if (postsError) console.error('Posts error:', postsError);

  return { user, posts };
}

Sequential vs Parallel Execution

Sequential Execution

// Each await waits for the previous one
async function sequential() {
  console.time('sequential');

  const user = await fetchUser(); // 1 second
  const posts = await fetchPosts(); // 1 second
  const comments = await fetchComments(); // 1 second

  console.timeEnd('sequential'); // ~3 seconds
  return { user, posts, comments };
}

Parallel Execution

// Method 1: Promise.all - fails if any promise rejects
async function parallel1() {
  console.time('parallel1');

  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments(),
  ]);

  console.timeEnd('parallel1'); // ~1 second
  return { user, posts, comments };
}

// Method 2: Start all, await individually
async function parallel2() {
  console.time('parallel2');

  // Start all requests
  const userPromise = fetchUser();
  const postsPromise = fetchPosts();
  const commentsPromise = fetchComments();

  // Await results
  const user = await userPromise;
  const posts = await postsPromise;
  const comments = await commentsPromise;

  console.timeEnd('parallel2'); // ~1 second
  return { user, posts, comments };
}

// Method 3: Promise.allSettled - doesn't fail if some reject
async function parallel3() {
  const results = await Promise.allSettled([
    fetchUser(),
    fetchPosts(),
    fetchComments(),
  ]);

  const user = results[0].status === 'fulfilled' ? results[0].value : null;
  const posts = results[1].status === 'fulfilled' ? results[1].value : [];
  const comments = results[2].status === 'fulfilled' ? results[2].value : [];

  return { user, posts, comments };
}

Advanced Patterns

Async Iteration

// Async generator function
async function* asyncGenerator() {
  yield await Promise.resolve(1);
  yield await Promise.resolve(2);
  yield await Promise.resolve(3);
}

// Using async iteration
async function useAsyncIterator() {
  for await (const value of asyncGenerator()) {
    console.log(value); // 1, 2, 3
  }
}

// Real-world 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;
  }
}

// Using paginated data
async function getAllItems() {
  const allItems = [];

  for await (const items of fetchPages('/api/items')) {
    allItems.push(...items);
  }

  return allItems;
}

Rate Limiting and Queuing

class RateLimiter {
  constructor(maxConcurrent = 5) {
    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 next = this.queue.shift();
      if (next) next();
    }
  }
}

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

async function fetchWithLimit(urls) {
  const promises = urls.map((url) =>
    limiter.run(() => fetch(url).then((r) => r.json()))
  );

  return Promise.all(promises);
}

Retry Logic

async function retry(fn, retries = 3, delay = 1000) {
  try {
    return await fn();
  } catch (error) {
    if (retries === 0) throw error;

    console.log(`Retrying... ${retries} attempts left`);
    await new Promise((resolve) => setTimeout(resolve, delay));

    return retry(fn, retries - 1, delay * 2); // Exponential backoff
  }
}

// Usage
async function fetchWithRetry(url) {
  return retry(
    () =>
      fetch(url).then((r) => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      }),
    3,
    1000
  );
}

Timeout Implementation

function timeout(ms) {
  return new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
}

async function fetchWithTimeout(url, ms = 5000) {
  try {
    const response = await Promise.race([fetch(url), timeout(ms)]);

    return await response.json();
  } catch (error) {
    if (error.message === 'Timeout') {
      console.error('Request timed out');
    }
    throw error;
  }
}

// Alternative using AbortController
async function fetchWithAbort(url, ms = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), ms);

  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      console.error('Request aborted');
    }
    throw error;
  }
}

Common Use Cases

API Calls

class ApiClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;

    try {
      const response = await fetch(url, {
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
        },
        ...options,
      });

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

      return await response.json();
    } catch (error) {
      console.error('API Request failed:', error);
      throw error;
    }
  }

  async get(endpoint) {
    return this.request(endpoint, { method: 'GET' });
  }

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

// Usage
const api = new ApiClient('https://api.example.com');

async function createUser(userData) {
  try {
    const user = await api.post('/users', userData);
    console.log('User created:', user);
    return user;
  } catch (error) {
    console.error('Failed to create user:', error);
  }
}

File Operations

// Reading files in Node.js
import { promises as fs } from 'fs';

async function readConfig() {
  try {
    const data = await fs.readFile('config.json', 'utf8');
    return JSON.parse(data);
  } catch (error) {
    console.error('Failed to read config:', error);
    return {};
  }
}

// Processing multiple files
async function processFiles(filePaths) {
  const results = await Promise.all(
    filePaths.map(async (path) => {
      const content = await fs.readFile(path, 'utf8');
      // Process content
      return processContent(content);
    })
  );

  return results;
}

Database Operations

class Database {
  async connect() {
    // Simulate connection
    await new Promise((resolve) => setTimeout(resolve, 100));
    console.log('Connected to database');
  }

  async query(sql, params) {
    // Simulate query
    await new Promise((resolve) => setTimeout(resolve, 50));
    return { rows: [] };
  }

  async transaction(callback) {
    await this.query('BEGIN');

    try {
      const result = await callback(this);
      await this.query('COMMIT');
      return result;
    } catch (error) {
      await this.query('ROLLBACK');
      throw error;
    }
  }
}

// Usage
async function transferMoney(db, fromId, toId, amount) {
  return db.transaction(async (tx) => {
    const from = await tx.query('SELECT balance FROM accounts WHERE id = ?', [
      fromId,
    ]);

    if (from.rows[0].balance < amount) {
      throw new Error('Insufficient funds');
    }

    await tx.query('UPDATE accounts SET balance = balance - ? WHERE id = ?', [
      amount,
      fromId,
    ]);
    await tx.query('UPDATE accounts SET balance = balance + ? WHERE id = ?', [
      amount,
      toId,
    ]);

    return { success: true, amount };
  });
}

Performance Considerations

// Bad: Sequential when parallel is possible
async function slowVersion(ids) {
  const results = [];
  for (const id of ids) {
    const data = await fetchData(id); // Each waits for previous
    results.push(data);
  }
  return results;
}

// Good: Parallel execution
async function fastVersion(ids) {
  const promises = ids.map((id) => fetchData(id));
  return Promise.all(promises);
}

// Bad: Creating unnecessary promises
async function unnecessary() {
  return await Promise.resolve(42); // await is unnecessary
}

// Good: Direct return
async function better() {
  return 42; // Automatically wrapped in Promise
}

// Memory consideration for large datasets
async function* processBatches(items, batchSize = 100) {
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const results = await Promise.all(batch.map((item) => processItem(item)));
    yield results;
  }
}

Common Pitfalls and Solutions

Forgetting await

// Pitfall: Missing await
async function bug() {
  const data = fetch('/api/data'); // Returns Promise, not data!
  console.log(data.name); // Error: Cannot read property 'name' of Promise
}

// Solution
async function fixed() {
  const response = await fetch('/api/data');
  const data = await response.json();
  console.log(data.name);
}

Array methods with async

// Pitfall: forEach doesn't wait
async function wrong() {
  const ids = [1, 2, 3];
  ids.forEach(async (id) => {
    await processId(id); // These run in parallel, not sequentially
  });
  console.log('Done'); // Logs before processing finishes
}

// Solution 1: for...of loop
async function correct1() {
  const ids = [1, 2, 3];
  for (const id of ids) {
    await processId(id); // Sequential
  }
  console.log('Done');
}

// Solution 2: Promise.all with map
async function correct2() {
  const ids = [1, 2, 3];
  await Promise.all(ids.map((id) => processId(id))); // Parallel
  console.log('Done');
}

Error swallowing

// Pitfall: Errors not propagated
async function swallowError() {
  try {
    await riskyOperation();
  } catch (error) {
    console.log(error); // Only logs, doesn't propagate
  }
}

// Solution: Re-throw when needed
async function propagateError() {
  try {
    await riskyOperation();
  } catch (error) {
    console.error('Operation failed:', error);
    throw error; // Re-throw for caller to handle
  }
}

Best Practices

  1. Always handle errors - Use try-catch or .catch()
  2. Prefer async/await over Promise chains for readability
  3. Use Promise.all() for parallel operations
  4. Don't await unnecessarily - Return promises directly when possible
  5. Be careful with loops - Understand sequential vs parallel execution
  6. Add timeouts for external requests
  7. Use proper error messages - Include context in errors
  8. Test async code properly - Use async test frameworks

Conclusion

Async/await has revolutionized asynchronous programming in JavaScript by making it more readable and maintainable. Key takeaways:

  • Async functions always return Promises
  • await pauses execution until Promise resolves
  • Proper error handling with try-catch is crucial
  • Understanding sequential vs parallel execution improves performance
  • Modern patterns like retry logic and rate limiting enhance robustness

Master async/await to write cleaner, more efficient asynchronous JavaScript code!