Nội dung

DaiPhan

DaiPhan

Full-Stack Developer

Full-stack developer passionate about modern web technologies, best practices, and sharing knowledge with the community.

Skills & Expertise

JavaScript TypeScript React Node.js DevOps
150+
Articles
50k+
Readers
4.9
Rating

JavaScript Async Patterns: Từ Callback đến Async/Await

Tìm hiểu các patterns xử lý asynchronous trong JavaScript: Callbacks, Promises, Async/Await và best practices

JavaScript Async Patterns: Từ Callback đến Async/Await

Asynchronous programming là skill thiết yếu cho JavaScript developer. Bài viết này covers tất cả patterns từ cơ bản đến nâng cao.

1. Callback Pattern (Legacy)

Basic Callback

// ❌ Callback hell
getUserData(userId, function(user) {
  getUserPosts(user.id, function(posts) {
    getPostComments(posts[0].id, function(comments) {
      getCommentAuthors(comments, function(authors) {
        console.log('Final result:', authors);
      });
    });
  });
});

// ✅ Named functions
function handleAuthors(authors) {
  console.log('Final result:', authors);
}

function handleComments(comments) {
  getCommentAuthors(comments, handleAuthors);
}

function handlePosts(posts) {
  getPostComments(posts[0].id, handleComments);
}

function handleUser(user) {
  getUserPosts(user.id, handlePosts);
}

getUserData(userId, handleUser);

Error-First Callback

function readFileAsync(filename, callback) {
  fs.readFile(filename, 'utf8', (error, data) => {
    if (error) {
      return callback(error);
    }
    
    try {
      const parsed = JSON.parse(data);
      callback(null, parsed);
    } catch (parseError) {
      callback(parseError);
    }
  });
}

// Usage
readFileAsync('config.json', (error, config) => {
  if (error) {
    console.error('Error reading file:', error);
    return;
  }
  
  console.log('Config loaded:', config);
});

2. Promise Pattern

Creating Promises

// ✅ Promise constructor
function delay(ms) {
  return new Promise((resolve, reject) => {
    if (ms < 0) {
      reject(new Error('Delay must be positive'));
      return;
    }
    
    setTimeout(() => {
      resolve(`Delayed for ${ms}ms`);
    }, ms);
  });
}

// ✅ Promise.resolve/reject
function fetchData(success) {
  if (success) {
    return Promise.resolve({ data: 'success' });
  }
  return Promise.reject(new Error('Failed to fetch'));
}

Promise Chaining

// ✅ Clean promise chain
authenticateUser(credentials)
  .then(user => getUserProfile(user.id))
  .then(profile => updateLastLogin(profile.userId))
  .then(result => {
    console.log('User authenticated successfully');
    return result;
  })
  .catch(error => {
    console.error('Authentication failed:', error);
    throw error;
  });

// ✅ Parallel execution
const userPromise = getUser(userId);
const postsPromise = getUserPosts(userId);
const commentsPromise = getUserComments(userId);

Promise.all([userPromise, postsPromise, commentsPromise])
  .then(([user, posts, comments]) => {
    return {
      user,
      posts,
      comments
    };
  })
  .then(data => renderDashboard(data))
  .catch(error => showError(error));

Advanced Promise Patterns

// ✅ Promise.race - Timeout pattern
function withTimeout(promise, timeoutMs) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Operation timed out')), timeoutMs);
  });
  
  return Promise.race([promise, timeoutPromise]);
}

// ✅ Promise.allSettled - Handle all results
async function fetchMultipleResources(urls) {
  const promises = urls.map(url => fetch(url));
  
  const results = await Promise.allSettled(promises);
  
  return results.map((result, index) => ({
    url: urls[index],
    status: result.status,
    value: result.status === 'fulfilled' ? result.value : result.reason
  }));
}

// ✅ Promise.any - First successful
async function fetchWithFallback(urls) {
  try {
    const response = await Promise.any(urls.map(url => fetch(url)));
    return response;
  } catch (error) {
    throw new Error('All requests failed');
  }
}

3. Async/Await Pattern (Modern)

Basic Async/Await

// ✅ Sequential execution
async function getUserDashboard(userId) {
  try {
    const user = await getUser(userId);
    const posts = await getUserPosts(userId);
    const comments = await getUserComments(userId);
    
    return {
      user,
      posts,
      comments
    };
  } catch (error) {
    console.error('Failed to load dashboard:', error);
    throw error;
  }
}

// ✅ Parallel execution with Promise.all
async function getUserDashboardParallel(userId) {
  try {
    const [user, posts, comments] = await Promise.all([
      getUser(userId),
      getUserPosts(userId),
      getUserComments(userId)
    ]);
    
    return {
      user,
      posts,
      comments
    };
  } catch (error) {
    console.error('Failed to load dashboard:', error);
    throw error;
  }
}

Error Handling Patterns

// ✅ Try-catch with async/await
async function safeApiCall(url, options = {}) {
  try {
    const response = await fetch(url, options);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    console.error('API call failed:', error);
    return { success: false, error: error.message };
  }
}

// ✅ Multiple error handling
async function fetchUserData(userId) {
  const results = await Promise.allSettled([
    fetchUser(userId),
    fetchUserPosts(userId),
    fetchUserComments(userId)
  ]);
  
  const [userResult, postsResult, commentsResult] = results;
  
  return {
    user: userResult.status === 'fulfilled' ? userResult.value : null,
    posts: postsResult.status === 'fulfilled' ? postsResult.value : [],
    comments: commentsResult.status === 'fulfilled' ? commentsResult.value : [],
    errors: results
      .map((result, index) => 
        result.status === 'rejected' ? 
          { task: ['user', 'posts', 'comments'][index], error: result.reason } : 
          null
      )
      .filter(Boolean)
  };
}

4. Advanced Async Patterns

Async Iterators

// ✅ Async generator
async function* fetchPaginatedData(url) {
  let nextUrl = url;
  
  while (nextUrl) {
    const response = await fetch(nextUrl);
    const data = await response.json();
    
    yield data.results;
    
    nextUrl = data.next;
  }
}

// ✅ Using async iterator
async function processAllData() {
  const dataGenerator = fetchPaginatedData('/api/data');
  
  for await (const page of dataGenerator) {
    console.log('Processing page:', page);
    await processPage(page);
  }
}

Async Pool Pattern

// ✅ Limit concurrent operations
async function asyncPool(poolLimit, items, iteratorFn) {
  const executing = [];
  const results = [];
  
  for (const item of items) {
    const promise = Promise.resolve().then(() => iteratorFn(item));
    results.push(promise);
    
    if (poolLimit <= items.length) {
      const executingPromise = promise.then(() => 
        executing.splice(executing.indexOf(executingPromise), 1)
      );
      executing.push(executingPromise);
      
      if (executing.length >= poolLimit) {
        await Promise.race(executing);
      }
    }
  }
  
  return Promise.all(results);
}

// Usage
async function downloadFiles(urls) {
  const downloadFile = async (url) => {
    const response = await fetch(url);
    return response.blob();
  };
  
  // Limit to 3 concurrent downloads
  return asyncPool(3, urls, downloadFile);
}

Retry Pattern

// ✅ Exponential backoff retry
async function retryWithBackoff(
  operation, 
  maxRetries = 3, 
  baseDelay = 1000
) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      if (attempt === maxRetries) {
        throw error;
      }
      
      const delay = baseDelay * Math.pow(2, attempt - 1);
      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
      
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Usage
async function reliableApiCall(url) {
  return retryWithBackoff(async () => {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  });
}

5. Real-world Examples

API Client with Retry and Timeout

class ApiClient {
  constructor(baseURL, options = {}) {
    this.baseURL = baseURL;
    this.timeout = options.timeout || 30000;
    this.maxRetries = options.maxRetries || 3;
    this.retryDelay = options.retryDelay || 1000;
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    
    const operation = async () => {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), this.timeout);
      
      try {
        const response = await fetch(url, {
          ...options,
          signal: controller.signal
        });
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        return response.json();
      } finally {
        clearTimeout(timeoutId);
      }
    };
    
    return retryWithBackoff(operation, this.maxRetries, this.retryDelay);
  }
  
  async get(endpoint) {
    return this.request(endpoint, { method: 'GET' });
  }
  
  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
  }
}

// Usage
const api = new ApiClient('https://api.example.com', {
  timeout: 10000,
  maxRetries: 2,
  retryDelay: 500
});

async function loadUserData(userId) {
  try {
    const [user, posts, comments] = await Promise.all([
      api.get(`/users/${userId}`),
      api.get(`/users/${userId}/posts`),
      api.get(`/users/${userId}/comments`)
    ]);
    
    return { user, posts, comments };
  } catch (error) {
    console.error('Failed to load user data:', error);
    throw error;
  }
}

Queue Processing with Async

class AsyncQueue {
  constructor(concurrency = 1) {
    this.concurrency = concurrency;
    this.running = 0;
    this.queue = [];
  }
  
  add(task) {
    this.queue.push(task);
    this.process();
  }
  
  async process() {
    if (this.running >= this.concurrency || this.queue.length === 0) {
      return;
    }
    
    this.running++;
    const task = this.queue.shift();
    
    try {
      await task();
    } catch (error) {
      console.error('Task failed:', error);
    } finally {
      this.running--;
      this.process();
    }
  }
}

// Usage
const queue = new AsyncQueue(3); // Process 3 tasks concurrently

// Add tasks to queue
for (let i = 0; i < 10; i++) {
  queue.add(async () => {
    console.log(`Processing task ${i}...`);
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log(`Task ${i} completed`);
  });
}

6. Performance Optimization

Debounce và Throttle

// ✅ Debounce - Delay execution
function debounce(func, delay) {
  let timeoutId;
  
  return function (...args) {
    clearTimeout(timeoutId);
    
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// ✅ Throttle - Limit execution rate
function throttle(func, limit) {
  let inThrottle;
  
  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

// Usage
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(async (query) => {
  const results = await searchAPI(query);
  displayResults(results);
}, 300);

searchInput.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

7. Best Practices Summary

Do’s and Don’ts

// ✅ Do: Use async/await for readability
async function processData() {
  const data = await fetchData();
  const processed = await processAsync(data);
  return processed;
}

// ❌ Don't: Mix callbacks and promises
function mixedApproach(callback) {
  fetchData()
    .then(data => {
      callback(null, data);
    })
    .catch(error => {
      callback(error);
    });
}

// ✅ Do: Handle errors properly
async function safeOperation() {
  try {
    return await riskyOperation();
  } catch (error) {
    console.error('Operation failed:', error);
    return fallbackValue;
  }
}

// ❌ Don't: Forget error handling
async function unsafeOperation() {
  return await riskyOperation(); // Unhandled rejection!
}

// ✅ Do: Use Promise.all for parallel operations
async function loadUserData(userId) {
  const [user, posts, comments] = await Promise.all([
    getUser(userId),
    getPosts(userId),
    getComments(userId)
  ]);
  
  return { user, posts, comments };
}

Kết luận

JavaScript async patterns đã evolve rất nhiều:

  1. Callbacks → Legacy, avoid when possible
  2. Promises → Good foundation, still useful
  3. Async/Await → Modern standard, prefer this
  4. Advanced patterns → Use when needed

💡 Pro tip: Luôn consider readability, error handling, và performance khi chọn async pattern. Async/await là best choice cho hầu hết cases, nhưng hiểu tất cả patterns giúp bạn handle mọi situation!


Bài viết này là phần đầu trong series về JavaScript patterns. Trong các bài tiếp theo, chúng ta sẽ explore design patterns, performance optimization, và advanced techniques.