Design.dev design.dev

JavaScript Promises & Async/Await Guide

A complete reference for asynchronous JavaScript. Master Promises, async/await syntax, error handling, and practical patterns for API calls, parallel execution, and more.

Callbacks (The Old Way)

Understanding callbacks helps appreciate why Promises and async/await are better.

Basic Callback

// Callback-based async code
function fetchUser(id, callback) {
  setTimeout(() => {
    callback({ id: id, name: 'John Doe' });
  }, 1000);
}

// Usage
fetchUser(1, (user) => {
  console.log(user);
});

Callback Hell (Pyramid of Doom)

// ❌ Nested callbacks = hard to read and maintain
fetchUser(1, (user) => {
  fetchPosts(user.id, (posts) => {
    fetchComments(posts[0].id, (comments) => {
      fetchLikes(comments[0].id, (likes) => {
        console.log(likes);
        // Keep nesting... 😱
      });
    });
  });
});

Callbacks are hard to chain, difficult to handle errors, and create deeply nested code.

Promises

A Promise represents a value that may be available now, in the future, or never.

Promise States

// A Promise has three states:
// 1. pending   - initial state
// 2. fulfilled - operation completed successfully
// 3. rejected  - operation failed

const promise = new Promise((resolve, reject) => {
  // pending...
  
  if (success) {
    resolve(value);  // fulfilled
  } else {
    reject(error);   // rejected
  }
});

Creating a Promise

// Basic Promise
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);
});

// Promise with conditional logic
function fetchData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url) {
        resolve({ data: 'Some data' });
      } else {
        reject(new Error('URL is required'));
      }
    }, 1000);
  });
}

// Immediately resolved Promise
const resolvedPromise = Promise.resolve('Immediate value');

// Immediately rejected Promise
const rejectedPromise = Promise.reject(new Error('Failed'));

Using Promises (.then)

// Handling success
myPromise
  .then((result) => {
    console.log(result); // 'Success!'
  });

// Handling errors
myPromise
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error);
  });

// Finally block (runs regardless of outcome)
myPromise
  .then((result) => console.log(result))
  .catch((error) => console.error(error))
  .finally(() => {
    console.log('Cleanup or loading state reset');
  });

Promise Chaining

// ✅ Much better than callbacks!
fetchUser(1)
  .then((user) => {
    console.log('User:', user);
    return fetchPosts(user.id);
  })
  .then((posts) => {
    console.log('Posts:', posts);
    return fetchComments(posts[0].id);
  })
  .then((comments) => {
    console.log('Comments:', comments);
  })
  .catch((error) => {
    console.error('Error:', error);
  })
  .finally(() => {
    console.log('All done!');
  });

// Returning values in chain
Promise.resolve(5)
  .then((num) => num * 2)      // 10
  .then((num) => num + 3)      // 13
  .then((num) => {
    console.log(num);          // 13
  });

Async/Await

Modern syntax that makes asynchronous code look and behave like synchronous code.

Async Functions

// async keyword makes a function return a Promise
async function fetchData() {
  return 'Hello';
}

// Equivalent to:
function fetchData() {
  return Promise.resolve('Hello');
}

// Usage
fetchData().then((result) => console.log(result)); // 'Hello'

// Arrow function
const getData = async () => {
  return 'Data';
};

Await Keyword

// await pauses execution until Promise resolves
async function getUser() {
  const response = await fetch('/api/user');
  const user = await response.json();
  return user;
}

// ✅ Clean, readable code
async function fetchAllData() {
  const user = await fetchUser(1);
  console.log('User:', user);
  
  const posts = await fetchPosts(user.id);
  console.log('Posts:', posts);
  
  const comments = await fetchComments(posts[0].id);
  console.log('Comments:', comments);
  
  return comments;
}

// await only works inside async functions
async function example() {
  const data = await fetchData(); // ✅ Works
}

// ❌ This won't work
function example() {
  const data = await fetchData(); // SyntaxError
}

Converting Promises to Async/Await

// Using .then
function getUserPosts() {
  return fetchUser(1)
    .then((user) => fetchPosts(user.id))
    .then((posts) => posts)
    .catch((error) => console.error(error));
}

// ✅ Using async/await (cleaner!)
async function getUserPosts() {
  try {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    return posts;
  } catch (error) {
    console.error(error);
  }
}

Top-Level Await (ES2022)

// In modules, you can use await at the top level
// (Outside of async functions)

// module.js
const data = await fetchData();
export default data;

// Or
export const users = await fetch('/api/users').then(r => r.json());

Error Handling

Proper error handling is crucial for robust async code.

Try/Catch with Async/Await

// Basic try/catch
async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw error; // Re-throw if needed
  }
}

// Multiple operations
async function getData() {
  try {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    return { user, posts };
  } catch (error) {
    console.error('Error in getData:', error);
    return null;
  }
}

// With finally
async function updateUser(id, data) {
  try {
    const response = await fetch(`/api/users/${id}`, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
    return await response.json();
  } catch (error) {
    console.error('Update failed:', error);
    throw error;
  } finally {
    console.log('Update attempt completed');
  }
}

Catch with Promises

// Using .catch()
fetchUser(1)
  .then((user) => fetchPosts(user.id))
  .then((posts) => console.log(posts))
  .catch((error) => {
    console.error('Error:', error);
  });

// Multiple catch blocks
fetchUser(1)
  .then((user) => {
    return fetchPosts(user.id);
  })
  .catch((error) => {
    console.error('User fetch failed:', error);
    return []; // Return default value
  })
  .then((posts) => {
    console.log('Posts:', posts);
  })
  .catch((error) => {
    console.error('Posts fetch failed:', error);
  });

Error Handling Patterns

// Wrap individual awaits for specific handling
async function fetchData() {
  let user, posts;
  
  try {
    user = await fetchUser(1);
  } catch (error) {
    console.error('User fetch failed:', error);
    user = { id: 1, name: 'Default User' };
  }
  
  try {
    posts = await fetchPosts(user.id);
  } catch (error) {
    console.error('Posts fetch failed:', error);
    posts = [];
  }
  
  return { user, posts };
}

// Custom error handling function
async function handleAsync(promise) {
  try {
    const data = await promise;
    return [null, data];
  } catch (error) {
    return [error, null];
  }
}

// Usage
const [error, user] = await handleAsync(fetchUser(1));
if (error) {
  console.error(error);
} else {
  console.log(user);
}

HTTP Error Handling

// fetch() doesn't reject on HTTP errors (404, 500, etc.)
// You need to check response.ok

async function fetchWithErrorHandling(url) {
  try {
    const response = await fetch(url);
    
    // Check if request was successful
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch failed:', error);
    throw error;
  }
}

// Reusable fetch wrapper
async function fetchJSON(url, options = {}) {
  const response = await fetch(url, options);
  
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || `HTTP ${response.status}`);
  }
  
  return response.json();
}

// Usage
try {
  const user = await fetchJSON('/api/user/1');
  console.log(user);
} catch (error) {
  console.error('Failed:', error.message);
}

Promise Methods

Powerful methods for handling multiple Promises.

Promise.all()

// Wait for ALL promises to resolve
// Rejects if ANY promise rejects

const promise1 = fetch('/api/users');
const promise2 = fetch('/api/posts');
const promise3 = fetch('/api/comments');

Promise.all([promise1, promise2, promise3])
  .then(([users, posts, comments]) => {
    console.log('All data loaded');
  })
  .catch((error) => {
    console.error('One failed:', error);
  });

// With async/await
async function loadAllData() {
  try {
    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 };
  } catch (error) {
    console.error('Failed to load data:', error);
  }
}

// Practical example: parallel requests
async function getUserData(userId) {
  const [profile, posts, followers] = await Promise.all([
    fetchProfile(userId),
    fetchPosts(userId),
    fetchFollowers(userId)
  ]);
  
  return { profile, posts, followers };
}

Promise.allSettled()

// Wait for ALL promises to complete (resolve OR reject)
// Never rejects, always resolves with array of results

const promises = [
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/invalid')  // This will fail
];

const results = await Promise.allSettled(promises);

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

// Result format:
// [
//   { status: 'fulfilled', value: ... },
//   { status: 'fulfilled', value: ... },
//   { status: 'rejected', reason: Error }
// ]

// Practical example: try multiple sources
async function fetchFromMultipleSources(id) {
  const results = await Promise.allSettled([
    fetchFromAPI1(id),
    fetchFromAPI2(id),
    fetchFromCache(id)
  ]);
  
  // Return first successful result
  const success = results.find(r => r.status === 'fulfilled');
  return success ? success.value : null;
}

Promise.race()

// Resolves/rejects as soon as ANY promise resolves/rejects

const promise1 = new Promise(resolve => setTimeout(() => resolve('slow'), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve('fast'), 100));

const result = await Promise.race([promise1, promise2]);
console.log(result); // 'fast'

// Timeout pattern
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 response;
  } catch (error) {
    console.error('Request timed out or failed:', error);
    throw error;
  }
}

// Usage
try {
  const data = await fetchWithTimeout('/api/slow-endpoint', 3000);
} catch (error) {
  console.log('Request took too long!');
}

Promise.any()

// Resolves as soon as ANY promise fulfills
// Rejects only if ALL promises reject

const promises = [
  fetch('/api/backup1'),
  fetch('/api/backup2'),
  fetch('/api/backup3')
];

try {
  const first = await Promise.any(promises);
  console.log('First successful response:', first);
} catch (error) {
  console.error('All requests failed:', error);
}

// Practical: try multiple mirrors
async function fetchFromMirrors(path) {
  const mirrors = [
    `https://cdn1.example.com${path}`,
    `https://cdn2.example.com${path}`,
    `https://cdn3.example.com${path}`
  ];
  
  try {
    const response = await Promise.any(
      mirrors.map(url => fetch(url))
    );
    return response;
  } catch (error) {
    throw new Error('All CDN mirrors failed');
  }
}

Comparison Table

// Promise.all()       - Wait for all, fail if any fails
// Promise.allSettled() - Wait for all, never fails
// Promise.race()      - First to finish (resolve or reject)
// Promise.any()       - First to succeed, fail if all fail

// Examples
await Promise.all([p1, p2, p3]);       // [v1, v2, v3] or throws
await Promise.allSettled([p1, p2, p3]);// [{...}, {...}, {...}]
await Promise.race([p1, p2, p3]);      // v1 (fastest)
await Promise.any([p1, p2, p3]);       // v1 (first success)

Fetch API

Modern way to make HTTP requests using Promises.

Basic GET Request

// Simple GET
async function getUsers() {
  const response = await fetch('/api/users');
  const users = await response.json();
  return users;
}

// With error handling
async function getUsers() {
  try {
    const response = await fetch('/api/users');
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const users = await response.json();
    return users;
  } catch (error) {
    console.error('Failed to fetch users:', error);
    throw error;
  }
}

// Check response type
async function fetchData(url) {
  const response = await fetch(url);
  
  const contentType = response.headers.get('content-type');
  
  if (contentType.includes('application/json')) {
    return response.json();
  } else if (contentType.includes('text/')) {
    return response.text();
  } else {
    return response.blob();
  }
}

POST Request

// POST with JSON
async function createUser(userData) {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(userData)
  });
  
  if (!response.ok) {
    throw new Error('Failed to create user');
  }
  
  return response.json();
}

// Usage
const newUser = await createUser({
  name: 'Jane Doe',
  email: '[email protected]'
});

Other HTTP Methods

// PUT (update)
async function updateUser(id, userData) {
  const response = await fetch(`/api/users/${id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData)
  });
  return response.json();
}

// PATCH (partial update)
async function patchUser(id, updates) {
  const response = await fetch(`/api/users/${id}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(updates)
  });
  return response.json();
}

// DELETE
async function deleteUser(id) {
  const response = await fetch(`/api/users/${id}`, {
    method: 'DELETE'
  });
  
  if (!response.ok) {
    throw new Error('Failed to delete user');
  }
  
  return true;
}

Request with Headers & Auth

// With authentication
async function fetchProtectedData() {
  const response = await fetch('/api/protected', {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });
  return response.json();
}

// Custom headers
async function fetchWithHeaders(url) {
  const response = await fetch(url, {
    headers: {
      'X-Custom-Header': 'value',
      'Accept': 'application/json',
      'Authorization': 'Bearer token123'
    }
  });
  return response.json();
}

// Reusable fetch with defaults
const api = {
  baseURL: '/api',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${getToken()}`
  },
  
  async get(endpoint) {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      headers: this.headers
    });
    return response.json();
  },
  
  async post(endpoint, data) {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify(data)
    });
    return response.json();
  }
};

// Usage
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John' });

Handling Different Response Types

// JSON response
const data = await response.json();

// Text response
const text = await response.text();

// Blob (for images, files)
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);

// ArrayBuffer (for binary data)
const buffer = await response.arrayBuffer();

// FormData
const formData = await response.formData();

// Clone response (can only read body once)
const response = await fetch('/api/data');
const clone = response.clone();
const data1 = await response.json();
const data2 = await clone.json();

Common Patterns

Practical async patterns you'll use frequently.

Sequential vs Parallel Execution

// ❌ Sequential (slow) - waits for each one
async function getDataSequential() {
  const users = await fetchUsers();     // Wait
  const posts = await fetchPosts();     // Wait
  const comments = await fetchComments(); // Wait
  // Total time: sum of all requests
  return { users, posts, comments };
}

// ✅ Parallel (fast) - all at once
async function getDataParallel() {
  const [users, posts, comments] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchComments()
  ]);
  // Total time: longest single request
  return { users, posts, comments };
}

// Mixed: sequential with dependencies
async function getUserWithPosts(userId) {
  const user = await fetchUser(userId);  // Must get user first
  const posts = await fetchPosts(user.id); // Then get their posts
  return { user, posts };
}

// Mixed: some parallel, some sequential
async function loadDashboard(userId) {
  // First, get user (needed for other requests)
  const user = await fetchUser(userId);
  
  // Then fetch everything else in parallel
  const [posts, friends, notifications] = await Promise.all([
    fetchPosts(user.id),
    fetchFriends(user.id),
    fetchNotifications(user.id)
  ]);
  
  return { user, posts, friends, notifications };
}

Retry Logic

// Retry on failure
async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url);
      if (response.ok) {
        return response.json();
      }
    } catch (error) {
      console.log(`Attempt ${i + 1} failed`);
      if (i === maxRetries - 1) throw error;
      
      // Wait before retry (exponential backoff)
      await new Promise(resolve => 
        setTimeout(resolve, Math.pow(2, i) * 1000)
      );
    }
  }
}

// Retry with exponential backoff
async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      
      const delay = baseDelay * Math.pow(2, i);
      console.log(`Retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Usage
const data = await retryWithBackoff(
  () => fetch('/api/unreliable').then(r => r.json()),
  3,
  1000
);

Debouncing Async Operations

// Search with debounce
let searchTimeout;

async function searchWithDebounce(query) {
  clearTimeout(searchTimeout);
  
  return new Promise((resolve) => {
    searchTimeout = setTimeout(async () => {
      const results = await fetch(`/api/search?q=${query}`)
        .then(r => r.json());
      resolve(results);
    }, 300); // Wait 300ms after last keystroke
  });
}

// Usage with input
input.addEventListener('input', async (e) => {
  const results = await searchWithDebounce(e.target.value);
  displayResults(results);
});

// Reusable debounce function
function debounce(fn, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    return new Promise((resolve) => {
      timeoutId = setTimeout(async () => {
        resolve(await fn(...args));
      }, delay);
    });
  };
}

// Usage
const debouncedSearch = debounce(searchAPI, 300);
const results = await debouncedSearch('query');

Loading States

// Manage loading, data, and error states
async function loadUserData(userId) {
  const state = {
    loading: true,
    data: null,
    error: null
  };
  
  try {
    state.data = await fetchUser(userId);
  } catch (error) {
    state.error = error.message;
  } finally {
    state.loading = false;
  }
  
  return state;
}

// React-style pattern
async function useAsync(asyncFn) {
  let loading = true;
  let data = null;
  let error = null;
  
  try {
    data = await asyncFn();
  } catch (err) {
    error = err;
  } finally {
    loading = false;
  }
  
  return { loading, data, error };
}

// Usage
const { loading, data, error } = await useAsync(
  () => fetch('/api/users').then(r => r.json())
);

if (loading) console.log('Loading...');
if (error) console.error('Error:', error);
if (data) console.log('Data:', data);

Polling

// Poll for updates
async function poll(fn, interval = 2000, maxAttempts = 10) {
  let attempts = 0;
  
  while (attempts < maxAttempts) {
    try {
      const result = await fn();
      if (result.complete) {
        return result;
      }
    } catch (error) {
      console.error('Poll failed:', error);
    }
    
    await new Promise(resolve => setTimeout(resolve, interval));
    attempts++;
  }
  
  throw new Error('Max polling attempts reached');
}

// Usage: check job status
async function waitForJobCompletion(jobId) {
  return poll(
    async () => {
      const response = await fetch(`/api/jobs/${jobId}`);
      return response.json();
    },
    2000,
    30
  );
}

// Poll until condition met
async function pollUntil(fn, condition, interval = 1000) {
  while (true) {
    const result = await fn();
    if (condition(result)) {
      return result;
    }
    await new Promise(resolve => setTimeout(resolve, interval));
  }
}

// Usage
const data = await pollUntil(
  () => fetch('/api/status').then(r => r.json()),
  (status) => status.ready === true,
  2000
);

Concurrent Limit

// Process array with concurrency limit
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;
}

// Usage
const urls = ['url1', 'url2', 'url3', ...];
const data = await processBatch(
  urls.map(url => () => fetch(url).then(r => r.json())),
  3 // Process 3 at a time
);

// More sophisticated concurrency control
async function pLimit(concurrency) {
  const queue = [];
  let activeCount = 0;
  
  const next = () => {
    activeCount--;
    
    if (queue.length > 0) {
      queue.shift()();
    }
  };
  
  const run = async (fn) => {
    activeCount++;
    
    try {
      return await fn();
    } finally {
      next();
    }
  };
  
  const enqueue = (fn) => {
    return new Promise((resolve) => {
      const run = () => {
        resolve(run(fn));
      };
      
      if (activeCount < concurrency) {
        run();
      } else {
        queue.push(run);
      }
    });
  };
  
  return enqueue;
}

// Usage
const limit = pLimit(3);
const urls = [...];
const results = await Promise.all(
  urls.map(url => limit(() => fetch(url).then(r => r.json())))
);

Best Practices

Always Handle Errors

// ❌ Bad: unhandled promise rejection
async function bad() {
  const data = await fetch('/api/data').then(r => r.json());
  return data;
}

// ✅ Good: proper error handling
async function good() {
  try {
    const response = await fetch('/api/data');
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    console.error('Failed to fetch data:', error);
    throw error;
  }
}

Avoid Mixing Promises and Async/Await

// ❌ Bad: mixing styles is confusing
async function mixed() {
  return fetchData()
    .then(data => processData(data))
    .then(result => result);
}

// ✅ Good: consistent async/await
async function consistent() {
  const data = await fetchData();
  const result = await processData(data);
  return result;
}

Don't Await Unnecessarily

// ❌ Bad: unnecessary await
async function bad() {
  return await fetchData();
}

// ✅ Good: just return the promise
async function good() {
  return fetchData();
}

// ❌ Bad: awaiting when you don't need the value
async function bad() {
  await doSomething();
  await doSomethingElse();
  // If these don't depend on each other...
}

// ✅ Good: parallel execution
async function good() {
  await Promise.all([
    doSomething(),
    doSomethingElse()
  ]);
}

Use Promise.all for Parallel Requests

// ❌ Bad: sequential (slow)
async function loadData() {
  const users = await fetchUsers();
  const posts = await fetchPosts();
  const comments = await fetchComments();
  return { users, posts, comments };
}

// ✅ Good: parallel (fast)
async function loadData() {
  const [users, posts, comments] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchComments()
  ]);
  return { users, posts, comments };
}

Clean Up Resources

// Use finally for cleanup
async function uploadFile(file) {
  let uploadInProgress = true;
  showLoader();
  
  try {
    const result = await upload(file);
    return result;
  } catch (error) {
    handleError(error);
    throw error;
  } finally {
    uploadInProgress = false;
    hideLoader(); // Always cleanup
  }
}

Add Timeouts to Prevent Hanging

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

// Usage
try {
  const data = await withTimeout(
    fetch('/api/slow'),
    5000
  );
} catch (error) {
  console.error('Request timed out or failed');
}

Common Mistakes to Avoid

// ❌ Mistake 1: Forgetting async keyword
function bad() {
  await fetchData(); // SyntaxError!
}

// ❌ Mistake 2: Not returning in Promise chain
async function bad() {
  fetchData()
    .then(data => {
      processData(data); // Not returned!
    });
}

// ❌ Mistake 3: Using await in loops unnecessarily
async function bad(items) {
  for (const item of items) {
    await processItem(item); // One at a time (slow)
  }
}

// ✅ Better: process in parallel
async function good(items) {
  await Promise.all(items.map(item => processItem(item)));
}

// ❌ Mistake 4: Not checking response.ok with fetch
async function bad() {
  const data = await fetch('/api/data').then(r => r.json());
  // Doesn't handle 404, 500, etc!
}

// ✅ Good: check response
async function good() {
  const response = await fetch('/api/data');
  if (!response.ok) throw new Error('HTTP error');
  return response.json();
}