CN

JavaScript Async/Await Patterns That Make Your Code Better

Learn practical async/await patterns in JavaScript. Real examples of error handling, parallel requests, sequential operations, and race conditions.

Async/await makes JavaScript async code look synchronous. Here's how to use it properly and avoid common pitfalls.

Basic Error Handling

Stop wrapping every async call in try/catch. Use this instead:

Error Handler Pattern
async function handleAsync(promise) {
  try {
    const data = await promise;
    return [data, null];
  } catch (error) {
    return [null, error];
  }
}

// Usage
const [data, error] = await handleAsync(fetch('/api/users'));
if (error) {
  console.error('Failed:', error.message);
  return;
}

Parallel Requests

Don't wait for one request to finish before starting another.

Multiple API Calls
// Bad - Sequential requests
const users = await fetchUsers();
const posts = await fetchPosts();
const comments = await fetchComments();

// Good - Parallel requests
const [users, posts, comments] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
  fetchComments()
]);

Handle errors in parallel requests:

Parallel with Error Handling
const promises = [fetchUsers(), fetchPosts(), fetchComments()];
const results = await Promise.allSettled(promises);

const data = results.map(result => {
  if (result.status === 'fulfilled') {
    return result.value;
  }
  console.error('Failed:', result.reason);
  return null;
});

Timeouts

Don't let requests hang forever:

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

// Usage
try {
  const data = await timeout(fetch('/api/slowEndpoint'));
} catch (error) {
  if (error.message === 'Timeout') {
    console.log('Request took too long');
  }
}

Loading States

Handle loading states cleanly:

Loading State Management
async function fetchWithLoading(fetchFn) {
  let isLoading = true;
  try {
    const result = await fetchFn();
    return result;
  } finally {
    isLoading = false;
  }
}

// React example
function UserList() {
  const [isLoading, setLoading] = useState(false);
  const [users, setUsers] = useState([]);

  async function loadUsers() {
    setLoading(true);
    try {
      const data = await fetch('/api/users');
      setUsers(await data.json());
    } finally {
      setLoading(false);
    }
  }
}

Retrying Failed Requests

When APIs are flaky, retry with backoff:

Retry with Backoff
async function fetchWithRetry(url, options = {}) {
  const { retries = 3, backoff = 300 } = options;
  
  for (let i = 0; i < retries; i++) {
    try {
      return await fetch(url);
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise(resolve => 
        setTimeout(resolve, backoff * Math.pow(2, i))
      );
    }
  }
}

// Usage
const data = await fetchWithRetry('/api/unstable-endpoint', {
  retries: 3,
  backoff: 500  // 500ms, 1000ms, 2000ms
});

Sequential Operations

When order matters:

Sequential Processing
async function processInOrder(items) {
  const results = [];
  for (const item of items) {
    // Using for...of preserves async/await
    const result = await processItem(item);
    results.push(result);
  }
  return results;
}

// With array methods - harder to read but sometimes needed
const results = await items.reduce(async (promise, item) => {
  const results = await promise;
  const result = await processItem(item);
  return [...results, result];
}, Promise.resolve([]));

Debouncing Async Calls

Perfect for search inputs:

Debounced API Calls
function debounce(fn, delay = 300) {
  let timeout;
  
  return (...args) => {
    clearTimeout(timeout);
    return new Promise(resolve => {
      timeout = setTimeout(() => resolve(fn(...args)), delay);
    });
  };
}

// Usage
const searchApi = async (term) => {
  const response = await fetch(`/api/search?q=${term}`);
  return response.json();
};

const debouncedSearch = debounce(searchApi, 500);

// In your input handler
input.addEventListener('input', async (e) => {
  const results = await debouncedSearch(e.target.value);
  displayResults(results);
});

Cache Results

Don't call the same API multiple times:

Async Result Caching
const cache = new Map();

async function fetchWithCache(url, options = {}) {
  const { ttl = 60000 } = options;  // Default 1 minute
  
  const cached = cache.get(url);
  if (cached?.timestamp > Date.now() - ttl) {
    return cached.data;
  }
  
  const data = await fetch(url).then(r => r.json());
  cache.set(url, {
    timestamp: Date.now(),
    data
  });
  
  return data;
}

// Usage
const data = await fetchWithCache('/api/frequently-accessed', {
  ttl: 5 * 60000  // 5 minutes
});

Race Conditions

Handle out-of-order responses:

Race Condition Prevention
function createAsyncSequence() {
  let currentRequest = 0;
  
  return async function request(url) {
    const thisRequest = ++currentRequest;
    const response = await fetch(url);
    
    // Ignore if newer request has started
    if (thisRequest !== currentRequest) {
      return null;
    }
    
    return response.json();
  };
}

// Usage in search
const sequentialSearch = createAsyncSequence();

input.addEventListener('input', async (e) => {
  const results = await sequentialSearch(
    `/api/search?q=${e.target.value}`
  );
  if (results) {
    displayResults(results);
  }
});

These patterns work great together. Here's a real-world example combining several:

Real World Example
async function userApi() {
  const cache = new Map();
  const sequence = createAsyncSequence();
  
  return {
    async search(term) {
      // Combine caching, sequence, timeout and error handling
      const cached = cache.get(term);
      if (cached?.timestamp > Date.now() - 30000) {
        return cached.data;
      }
      
      const [data, error] = await handleAsync(
        sequence(
          timeout(
            fetch(`/api/users/search?q=${term}`)
          )
        )
      );
      
      if (error) return [];
      
      const results = await data.json();
      cache.set(term, {
        timestamp: Date.now(),
        data: results
      });
      
      return results;
    }
  };
}
Want to read more articles like that?

Sign up to get notified when I publish more.

No spam. One click unsubscribe.

Read More on Fado Code Camp