Chương 06 · Async JavaScript

Bất đồng bộ: Callbacks, Promise, async/await

JavaScript chỉ có một thread nhưng vẫn handle được hàng ngàn request đồng thời — nhờ event loop. Hiểu sâu cơ chế Call Stack, Web API, Microtask Queue và Macrotask Queue là chìa khoá để viết code async không bug và không block UI. Đây là chương khó nhất của JavaScript.

Độ dài: ~1100 dòng Bài tập: 5 Quiz: 8 Prerequisites: Chương 1-5
🎯 Mục tiêu chương
  • Hiểu nghịch lý single-threaded + non-blocking của JS.
  • Vẽ và trace được event loop với Call Stack, Web API, Macrotask Q, Microtask Q.
  • Hiểu callback pattern, callback hell và inversion of control.
  • Master Promise 3 trạng thái, chaining, error propagation.
  • Phân biệt Promise.all, allSettled, race, any.
  • Viết async/await đúng — phân biệt sequential vs parallel.
  • Xử lý lỗi async đúng cách: try/catch, .catch, unhandledrejection.
  • Dùng AbortController để cancel async operation.
  • Hiểu vì sao async ≠ parallel — CPU-bound vẫn block.

1. Nghịch lý: Single-threaded nhưng Non-blocking

JavaScript được thiết kế năm 1995 để chạy trong trình duyệt — nơi UI thread chỉ có một. Nếu một đoạn code chiếm thread quá lâu, cả trang web "đứng hình": không click được, không scroll được. Vì vậy JS quyết định: không bao giờ block.

Nhưng JS chỉ có một thread để chạy code. Vậy làm sao một server Node.js có thể handle 10000 connection cùng lúc?

🧠 Mental model — JS là người pha chế

Tưởng tượng một quán cà phê chỉ có một nhân viên (JS thread). Khách đặt cappuccino — nhân viên không đứng chờ máy espresso 30 giây. Thay vào đó: bấm máy, để đó, quay sang phục vụ khách tiếp theo. Khi máy kêu "ting", nhân viên quay lại lấy cốc.

Nhân viên = JS thread (single). Máy espresso = Web API (network, timer, file I/O) chạy ngoài JS thread, thường được implement bằng C++ trong V8/Node. Tiếng "ting" = callback được đẩy vào queue. Việc quay lại lấy cốc = event loop.

Bí mật: JS engine (V8, SpiderMonkey...) thực ra chỉ chạy JS code. Mọi tác vụ chậm (HTTP request, setTimeout, đọc file) được delegate ra môi trường host (browser hoặc Node):

  • Trình duyệt: Web APIs — DOM, fetch, setTimeout, XMLHttpRequest, IndexedDB...
  • Node.js: libuv (C library) cung cấp thread pool cho file I/O, DNS, crypto + non-blocking I/O cho network.

JS thread chỉ làm 2 việc: (1) chạy code đồng bộ, (2) khi rảnh thì kéo callback từ queue ra chạy. Suốt thời gian tác vụ chậm đang diễn ra ngoài JS thread, JS vẫn rảnh để xử lý code khác.

2. Event Loop — trái tim của async JS

Event loop là vòng lặp vô hạn, mỗi tick gồm 4 thành phần:

Call Stack Web/Node API Macrotask Queue Microtask Queue
Stack LIFO. Function nào đang chạy nằm trên đỉnh. Môi trường host (browser/Node). Chạy ngoài JS thread. FIFO. Chứa callback từ setTimeout, setInterval, I/O, UI event. FIFO. Chứa callback từ Promise.then, queueMicrotask, MutationObserver.
Mỗi function call push 1 frame; return pop frame. Timer đếm, network fetch, file read... Khi API xong, push callback vào đây. Priority cao hơn macrotask.
Stack rỗng = JS rảnh. Hoàn thành → đẩy callback vào queue tương ứng. 1 macrotask/tick. Drain toàn bộ trước macrotask kế.

Thuật toán event loop (mỗi tick):

  1. Chờ Call Stack rỗng.
  2. Drain microtask queue hoàn toàn — chạy tất cả microtask hiện có, kể cả microtask mới được push trong lúc drain.
  3. Lấy đúng 1 macrotask từ macrotask queue, push vào stack, chạy đến khi stack rỗng.
  4. Render UI (browser) nếu cần.
  5. Lặp lại từ bước 1.
📘 Quy tắc vàng

Sau mỗi macrotask, tất cả microtask được drain trước macrotask kế. Đó là lý do Promise.then luôn chạy trước setTimeout(..., 0) khi cả hai được schedule cùng lúc.

2.1. Trace chi tiết — từng tick

Code sau:

Trace target
console.log('A');

setTimeout(() => console.log('B'), 0);

Promise.resolve().then(() => console.log('C'));

console.log('D');

Trace state qua từng bước:

Tick Call Stack Web API Macrotask Q Microtask Q Output
1main, console.log('A')A
2main, setTimeout(...)Timer 0ms
3main, Promise.then(...)Timer 0mscb-C
4main, console.log('D')Timer 0ms expire → pushcb-Bcb-CD
5rỗngcb-Bcb-C
6cb-C (drain micro)cb-BrỗngC
7rỗngcb-Brỗng
8cb-B (1 macro)rỗngrỗngB

Kết quả output: A, D, C, B.

💡 Mnemonic

"Sync trước, Micro sau, Macro cuối". Khi đọc code, tách thành 3 nhóm theo thứ tự thực thi: (1) statement đồng bộ, (2) callback Promise/queueMicrotask, (3) callback setTimeout/setInterval/I/O.

3. Callback pattern — cách cũ làm async

Trước khi có Promise (ES6), JS làm async bằng callback: truyền function vào API, API gọi lại khi xong.

Callback cơ bản
// setTimeout là API callback đơn giản nhất
setTimeout(() => {
  console.log('1 giây sau');
}, 1000);

// Node.js style: error-first callback
fs.readFile('data.txt', 'utf8', (err, data) => {
  if (err) return console.error(err);
  console.log(data);
});

3.1. Callback hell — kim tự tháp doom

Khi cần nhiều async tuần tự, callback lồng nhau thành "pyramid of doom":

Callback hell
getUser(userId, (err, user) => {
  if (err) return handleError(err);
  getPosts(user.id, (err, posts) => {
    if (err) return handleError(err);
    getComments(posts[0].id, (err, comments) => {
      if (err) return handleError(err);
      getAuthor(comments[0].authorId, (err, author) => {
        if (err) return handleError(err);
        console.log(author);
        // thụt vào sâu, error xử lý lặp lại, khó debug
      });
    });
  });
});

3.2. Inversion of Control — vấn đề tin cậy

🔥 Inversion of Control

Khi bạn truyền callback cho thư viện bên thứ 3, bạn đảo ngược quyền điều khiển — thư viện quyết định khi nào gọi, gọi bao nhiêu lần, gọi với tham số gì.

Câu hỏi không trả lời được khi nhìn API callback:

  • Callback được gọi 0 lần? (lib quên gọi)
  • Callback gọi nhiều hơn 1 lần? (lib bug)
  • Callback gọi đồng bộ hay bất đồng bộ? (Zalgo problem)
  • Lỗi được pass dưới dạng nào? (err đầu, throw, hay return value?)

Promise giải quyết bằng cách chuẩn hoá: luôn chỉ resolve/reject đúng 1 lần, luôn async, error theo 1 channel rõ ràng.

4. Promise — 3 trạng thái

Promise là đối tượng đại diện cho giá trị chưa có ở thời điểm hiện tại nhưng sẽ có trong tương lai. Có đúng 3 trạng thái và không thể đảo ngược:

Trạng tháiÝ nghĩaCó thể chuyển sang
PendingMới tạo, chưa xongFulfilled hoặc Rejected
FulfilledHoàn thành thành công với valueKhông thể chuyển
RejectedThất bại với reasonKhông thể chuyển

Một Promise settled (đã chốt) khi đã Fulfilled hoặc Rejected — không bao giờ quay lại Pending.

4.1. Tạo Promise

new Promise(...)
const p = new Promise((resolve, reject) => {
  // Đây là "executor" — chạy đồng bộ ngay lập tức
  setTimeout(() => {
    const ok = Math.random() > 0.5;
    if (ok) resolve(42);            // → Fulfilled với value 42
    else   reject(new Error('Hỏng'));  // → Rejected với reason
  }, 1000);
});

// Helper tạo sẵn:
Promise.resolve(42);              // Fulfilled ngay với 42
Promise.reject(new Error('x'));  // Rejected ngay

4.2. Consume bằng .then / .catch / .finally

Consume Promise
p.then(
  (value) => console.log('OK:', value),  // onFulfilled
  (reason) => console.error('Lỗi:', reason) // onRejected
);

// Dạng phổ biến hơn: tách then/catch
p
  .then((value) => console.log('OK:', value))
  .catch((reason) => console.error('Lỗi:', reason))
  .finally(() => console.log('Xong, dù OK hay lỗi'));
📘 Callback của .then luôn async

Kể cả khi gọi .then trên một Promise đã Fulfilled, callback không chạy đồng bộ ngay — nó được đẩy vào microtask queue và chạy ở cuối tick. Đây là một bảo đảm của spec, tránh Zalgo problem.

5. Promise chaining — phá vỡ kim tự tháp

.then() luôn trả về một Promise mới. Giá trị return trong callback then trở thành value của Promise tiếp theo. Nếu return Promise, chain sẽ auto-flatten — chờ Promise đó settle trước khi tiếp tục.

Promise chain — phá callback hell
getUser(userId)
  .then(user => getPosts(user.id))          // return Promise → flatten
  .then(posts => getComments(posts[0].id))
  .then(comments => getAuthor(comments[0].authorId))
  .then(author => console.log(author))
  .catch(handleError);  // 1 catch bắt mọi lỗi trong chain
🧠 Mental model — chain

Mỗi .then là một "trạm chuyển tiếp". Value đi vào, xử lý, value mới đi ra. Nếu return một Promise, trạm sẽ "đợi" Promise đó settle rồi mới pass value cho trạm kế. Lỗi ở bất kỳ trạm nào sẽ "skip" hết các trạm .then và rớt vào .catch gần nhất.

Quy tắc return trong .then
// 1. Return value thường → Promise.resolve(value)
Promise.resolve(1)
  .then(x => x + 1)        // → Promise<2>
  .then(x => console.log(x));  // 2

// 2. Return Promise → auto flatten
Promise.resolve(1)
  .then(x => Promise.resolve(x + 10))
  .then(x => console.log(x));  // 11

// 3. Throw trong then → next .catch
Promise.resolve(1)
  .then(() => { throw new Error('oops'); })
  .then(() => console.log('skip'))    // bỏ qua
  .catch(err => console.error(err.message));  // 'oops'

6. Promise combinators — chạy nhiều Promise

4 hàm tĩnh của Promise để kết hợp nhiều Promise:

Hàm Input Output Khi 1 cái reject Khi tất cả reject
Promise.all Array Promise Array value (cùng thứ tự) Short-circuit reject ngay Reject lỗi đầu tiên
Promise.allSettled Array Promise Array {status, value} hoặc {status, reason} Chờ tất cả, vẫn settled Chờ tất cả, vẫn settled
Promise.race Array Promise Value/reason của cái settle đầu tiên Reject nếu cái đó là reject Reject (cái đầu)
Promise.any Array Promise Value của cái resolve đầu tiên Bỏ qua, chờ resolve Reject AggregateError

6.1. Promise.all — "tất cả phải xong"

Promise.all
const [user, posts, settings] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchSettings(id),
]);
// 3 request chạy song song, kết quả theo đúng thứ tự

// Cảnh báo: 1 lỗi → all reject, kết quả 2 cái kia bị mất
try {
  await Promise.all([ok1(), fail(), ok2()]);
} catch (e) {
  console.error(e);  // chỉ thấy lỗi của fail(), không biết ok1/ok2
}

6.2. Promise.allSettled — "biết hết, dù tốt xấu"

Promise.allSettled
const results = await Promise.allSettled([
  fetchUser(id),
  fetchPosts(id),
  fetchSettings(id),
]);

for (const r of results) {
  if (r.status === 'fulfilled') console.log('OK:', r.value);
  else                            console.error('Lỗi:', r.reason);
}
// Hữu ích cho: dashboard có nhiều widget độc lập,
// 1 widget lỗi không nên làm sập cả page.

6.3. Promise.racePromise.any

race vs any
// race: cái nào settle đầu (resolve hay reject) → kết quả đó
const result = await Promise.race([
  fetchData(),
  new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 3000)),
]);
// Pattern timeout: cái nào nhanh hơn thắng

// any: cái nào resolve đầu (bỏ qua reject)
try {
  const winner = await Promise.any([
    fetch('https://mirror1.example/data'),
    fetch('https://mirror2.example/data'),
    fetch('https://mirror3.example/data'),
  ]);
  // Lấy mirror nào trả lời thành công đầu tiên
} catch (e) {
  // AggregateError nếu TẤT CẢ đều reject
  console.error(e.errors);
}
🔀 Khi nào chọn cái nào
  • all: cần tất cả thành công (vd: load đủ data trước khi render).
  • allSettled: muốn biết hết kết quả, không quan tâm fail (vd: dashboard nhiều widget).
  • race: lấy cái nhanh nhất (timeout, fastest mirror).
  • any: chỉ cần 1 cái resolve (failover, redundant request).

7. async/await — đường cú pháp ngọt

async/await (ES2017) là syntactic sugar trên Promise. Cho phép viết code async trông giống code đồng bộ — đọc và debug dễ hơn rất nhiều.

2 quy tắc cốt lõi:

  1. async function luôn trả về một Promise. Return value bình thường → Promise resolve. Throw → Promise reject.
  2. await chỉ dùng trong async function (hoặc top-level module). await promise tạm dừng function, đợi Promise settle, rồi tiếp tục.
async/await cơ bản
async function loadProfile(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchPosts(user.id);
  const author = await fetchAuthor(posts[0].authorId);
  return author;
}

// Tương đương Promise chain:
function loadProfile(userId) {
  return fetchUser(userId)
    .then(user => fetchPosts(user.id))
    .then(posts => fetchAuthor(posts[0].authorId));
}

// Async function trả Promise:
loadProfile(1).then(console.log);  // vẫn dùng .then được
💡 async function luôn trả Promise
async function foo() { return 1; }
foo();              // Promise<1>, KHÔNG phải 1
await foo();        // 1 (cần ở trong async hoặc top-level module)

async function bar() { throw new Error('x'); }
bar();              // Promise rejected với Error('x')

8. Sequential vs Parallel — bẫy hiệu năng

Đây là lỗi cực kỳ phổ biến của developer mới dùng async/await.

Sequential — CHẬM
// Mỗi await dừng function. 3 request chạy NỐI TIẾP nhau.
// Tổng thời gian = sum(t1, t2, t3) ≈ 3 giây nếu mỗi cái 1 giây.
async function loadAll() {
  const a = await fetch(url1);
  const b = await fetch(url2);
  const c = await fetch(url3);
  return [a, b, c];
}
Parallel — NHANH
// Khởi tạo 3 Promise NGAY, rồi await Promise.all.
// Tổng thời gian = max(t1, t2, t3) ≈ 1 giây.
async function loadAll() {
  const [a, b, c] = await Promise.all([
    fetch(url1),
    fetch(url2),
    fetch(url3),
  ]);
  return [a, b, c];
}

// Hoặc cách viết khác — cũng song song:
async function loadAll2() {
  const pa = fetch(url1);  // kick off ngay
  const pb = fetch(url2);
  const pc = fetch(url3);
  return [await pa, await pb, await pc];
}
🔥 for...of + await là tuần tự
// CHẬM — chạy lần lượt từng URL
for (const url of urls) {
  const data = await fetch(url);
  console.log(data);
}

// NHANH — song song toàn bộ
const all = await Promise.all(urls.map(fetch));
all.forEach(d => console.log(d));

Khi nào cần sequential thực sự? Khi mỗi bước phụ thuộc kết quả bước trước, hoặc khi cần rate-limit để không spam server. Khi không phụ thuộc — dùng Promise.all.

9. Error handling — bẫy phổ biến

3 cách xử lý lỗi async:

try/catch quanh await
async function main() {
  try {
    const data = await fetchData();
    return data;
  } catch (err) {
    console.error('Lỗi:', err);
    return null;
  } finally {
    cleanup();
  }
}
.catch trên Promise
fetchData()
  .then(data => render(data))
  .catch(err => console.error(err));

// Tip: gắn .catch ở cuối mọi chain để bắt lỗi propagate
🔥 try/catch KHÔNG bắt lỗi trong setTimeout
try {
  setTimeout(() => {
    throw new Error('oops');
  }, 0);
} catch (e) {
  console.log('KHÔNG chạy');  // catch không bắt được
}

Vì callback của setTimeout chạy ở tick khác, lúc đó stack đã không còn frame try bao quanh. Cách xử lý: try/catch ngay bên trong callback, hoặc wrap setTimeout thành Promise.

// Đúng: try/catch bên trong callback
setTimeout(() => {
  try { doRisky(); }
  catch (e) { console.error(e); }
}, 0);

9.1. Unhandled rejection

Promise reject mà không có .catch → unhandled rejection. Trong Node.js (v15+), mặc định crash process.

Bắt unhandled rejection
// Browser
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled:', event.reason);
  event.preventDefault();  // tránh log mặc định
});

// Node.js
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled:', reason);
});

10. AbortController — cancel async

Promise không có cơ chế cancel built-in. AbortController (ES2017+, hỗ trợ rộng từ 2019) là standard cho cancel: tạo controller, lấy signal truyền vào API, gọi abort() khi cần dừng.

AbortController với fetch
const controller = new AbortController();
const { signal } = controller;

fetch('https://example.com/big-file', { signal })
  .then(r => r.json())
  .then(console.log)
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Đã huỷ');
    } else {
      console.error(err);
    }
  });

// 5 giây sau: huỷ
setTimeout(() => controller.abort(), 5000);
Pattern timeout + abort
async function fetchWithTimeout(url, ms) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), ms);
  try {
    const res = await fetch(url, { signal: ctrl.signal });
    return await res.json();
  } finally {
    clearTimeout(timer);
  }
}

// ES2022: AbortSignal.timeout(ms) làm gọn hơn nữa
await fetch(url, { signal: AbortSignal.timeout(5000) });

11. Microtask vs Macrotask — trace cụ thể

Quy tắc đã nêu: microtask drain hết trước khi pick 1 macrotask. Ví dụ minh hoạ:

Thứ tự interleave
console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => {
  console.log('3');
  Promise.resolve().then(() => console.log('4'));
});

setTimeout(() => console.log('5'), 0);

Promise.resolve().then(() => console.log('6'));

console.log('7');

// Output: 1, 7, 3, 6, 4, 2, 5

Giải thích:

  1. Đồng bộ chạy hết: 1, 7.
  2. Drain micro queue. Queue có [then-3, then-6]. Chạy 3 — schedule thêm then-4 vào queue. Tiếp tục drain: 6, rồi 4.
  3. Pick 1 macrotask: 2. Drain micro (rỗng). Pick tiếp: 5.
⚠️ Microtask "starvation"

Nếu một microtask schedule thêm microtask, và microtask đó schedule thêm... → macrotask không bao giờ được chạy. Trong browser, UI sẽ đóng băng vì render là sau macrotask. Tránh recurse vô tận trong Promise.then.

12. async ≠ parallel — CPU-bound vẫn block

async chỉ "có thể await". Nó không tự chạy song song. Khi function async không gặp await, nó chạy đồng bộ y như function thường. Quan trọng nhất: CPU-bound code vẫn block thread.

CPU-bound trong async vẫn block
async function heavy() {
  let sum = 0;
  for (let i = 0; i < 1e10; i++) sum += i;  // block 10+ giây
  return sum;
}

// Gọi heavy() trong browser → UI freeze hoàn toàn.
// async KHÔNG biến CPU-bound thành non-blocking.

Để chạy CPU-bound thật sự song song, cần Web Worker (browser) hoặc Worker Threads (Node.js) — chạy code trên thread riêng, giao tiếp qua postMessage.

Web Worker (browser)
// main.js
const worker = new Worker('./heavy-worker.js');
worker.postMessage({ start: 0, end: 1e10 });
worker.onmessage = (e) => console.log('kết quả', e.data);

// heavy-worker.js — chạy trên thread riêng
onmessage = (e) => {
  let sum = 0;
  for (let i = e.data.start; i < e.data.end; i++) sum += i;
  postMessage(sum);
};
🔀 async vs threading
  • async: I/O không block (network, file). 1 thread đủ vì I/O chạy ở host.
  • Web Worker / Worker Threads: thread thật, song song CPU-bound.
  • Cluster (Node): nhiều process, mỗi process là 1 V8 instance, share port qua master.

Bài tập

Bài 1 — delay(ms)

Viết function delay(ms) trả Promise resolve sau ms mili giây. Dùng để pause trong async function.

async function demo() {
  console.log('start');
  await delay(1000);
  console.log('1s later');
}
Đáp án
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Đây là một trong những helper được dùng nhiều nhất. Promise resolve không cần value cũng OK — caller chỉ quan tâm khi nào.

Bài 2 — fetchWithTimeout(url, ms)

Viết hàm fetch nhưng có timeout: nếu sau ms mili giây mà chưa có response, reject với Error('timeout'). Yêu cầu thực sự huỷ request (không chỉ "bỏ qua kết quả").

2 cách giải

Cách 1 — dùng Promise.race (không huỷ thật):

function fetchWithTimeout(url, ms) {
  return Promise.race([
    fetch(url),
    new Promise((_, rej) =>
      setTimeout(() => rej(new Error('timeout')), ms)
    ),
  ]);
}

Cách 2 — dùng AbortController (huỷ thật):

async function fetchWithTimeout(url, ms) {
  const ctrl = new AbortController();
  const t = setTimeout(() => ctrl.abort(), ms);
  try {
    return await fetch(url, { signal: ctrl.signal });
  } catch (e) {
    if (e.name === 'AbortError') throw new Error('timeout');
    throw e;
  } finally {
    clearTimeout(t);
  }
}

Cách 2 tốt hơn vì giải phóng tài nguyên server-side ngay khi timeout.

Bài 3 — parallelLimit(tasks, limit)

Cho mảng tasks các async function (mỗi cái không có tham số, return Promise). Chạy chúng song song nhưng không quá limit cái cùng lúc. Trả về mảng kết quả đúng thứ tự.

const tasks = urls.map(u => () => fetch(u));
const results = await parallelLimit(tasks, 5);
Đáp án
async function parallelLimit(tasks, limit) {
  const results = new Array(tasks.length);
  let next = 0;
  async function worker() {
    while (next < tasks.length) {
      const i = next++;
      results[i] = await tasks[i]();
    }
  }
  const workers = Array.from({ length: Math.min(limit, tasks.length) }, worker);
  await Promise.all(workers.map(w => w()));
  return results;
}

Pattern: tạo limit worker, mỗi worker tự pick task tiếp theo từ chỉ số chung next. Khi tasks rỗng, worker exit. Promise.all chờ tất cả worker kết thúc.

Bài 4 — Refactor callback hell

Đoạn callback 4 tầng sau, refactor sang (a) Promise chain và (b) async/await:

getUser(1, (err, u) => {
  if (err) return cb(err);
  getPosts(u.id, (err, ps) => {
    if (err) return cb(err);
    getComments(ps[0].id, (err, cs) => {
      if (err) return cb(err);
      getAuthor(cs[0].authorId, (err, a) => {
        if (err) return cb(err);
        cb(null, a);
      });
    });
  });
});
Đáp án

Bước 1: promisify các function callback (Node.js có util.promisify):

const { promisify } = require('util');
const pGetUser     = promisify(getUser);
const pGetPosts    = promisify(getPosts);
const pGetComments = promisify(getComments);
const pGetAuthor   = promisify(getAuthor);

(a) Promise chain:

pGetUser(1)
  .then(u => pGetPosts(u.id))
  .then(ps => pGetComments(ps[0].id))
  .then(cs => pGetAuthor(cs[0].authorId))
  .then(a => cb(null, a))
  .catch(cb);

(b) async/await:

async function run() {
  const u  = await pGetUser(1);
  const ps = await pGetPosts(u.id);
  const cs = await pGetComments(ps[0].id);
  const a  = await pGetAuthor(cs[0].authorId);
  return a;
}

So sánh: callback 14 dòng nested, Promise 6 dòng phẳng, async/await 5 dòng tự nhiên như đồng bộ. Error handling: 4 lần lặp if (err) giảm còn 1 catch.

Bài 5 — Trace thứ tự log

Không chạy, đoán output:

console.log('A');

setTimeout(() => console.log('B'), 0);

Promise.resolve().then(() => {
  console.log('C');
  setTimeout(() => console.log('D'), 0);
});

setTimeout(() => {
  console.log('E');
  Promise.resolve().then(() => console.log('F'));
}, 0);

Promise.resolve().then(() => console.log('G'));

console.log('H');
Đáp án + giải thích

Output: A, H, C, G, B, E, F, D.

  • Sync: A, H.
  • Drain micro: C (schedule macro D), G.
  • 1 macro: B. Drain micro (rỗng).
  • 1 macro: E (schedule micro F). Drain micro: F.
  • 1 macro: D.

Quiz

Q1

Output của:

console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
Xem đáp án
✓ Đáp án

1, 4, 3, 2. Sync chạy trước (1, 4). Microtask (then) drain trước macrotask (setTimeout) → 3 rồi 2.

Q2

async function foo() { return 1; } — gọi foo() trả gì?

Xem đáp án
✓ Đáp án

Promise<1>, KHÔNG phải 1. Async function luôn trả Promise. Muốn lấy giá trị: await foo() hoặc foo().then(...).

Q3

try { setTimeout(() => { throw new Error('x'); }, 0) } catch (e) { ... }catch bắt được lỗi không?

Xem đáp án
✓ Đáp án

Không. Callback của setTimeout chạy ở tick khác, lúc đó stack đã không còn frame try bao quanh. Lỗi trở thành "uncaught exception" và bị bắt bởi window.onerror / process.on('uncaughtException'). Đặt try/catch bên trong callback mới bắt được.

Q4

await Promise.all([]) trả gì?

Xem đáp án
✓ Đáp án

[] — array rỗng. Promise.all([]) resolve ngay lập tức (đã ở microtask kế) với mảng rỗng. Edge case hữu ích khi map array có thể rỗng.

Q5

await Promise.race([]) trả gì?

Xem đáp án
✓ Đáp án

Pending mãi mãi — function bị treo. Vì race chờ "cái đầu tiên settle" mà không có cái nào để chờ. Đây là gotcha: luôn check array trước khi truyền vào race.

Q6

for await (const x of asyncIterable) có hoạt động không? Khi nào dùng?

Xem đáp án
✓ Đáp án

. for await...of (ES2018) lặp qua asyncIterable — object có [Symbol.asyncIterator](). Dùng cho stream (Node.js ReadableStream), paginated API, generator async. Mỗi lượt lặp await giá trị kế trước khi tiếp tục.

Q7

Gọi async function có tự động chạy song song nhau không?

Xem đáp án
✓ Đáp án

Không. async chỉ là "có thể chứa await". Vẫn chạy trên cùng một thread JS. Concurrency của async đến từ việc I/O delegate ra Web/Node API, không phải từ thread thật. CPU-bound trong async vẫn block thread — cần Worker để chạy song song thật sự.

Q8

Promise.allSettled giải quyết hạn chế gì của Promise.all?

Xem đáp án
✓ Đáp án

Promise.all short-circuit reject ngay khi có 1 cái fail — kết quả của các Promise khác bị mất (mặc dù chúng vẫn chạy trong background). Promise.allSettled chờ tất cả settle và trả mảng {status, value/reason}, không bao giờ reject. Phù hợp khi muốn biết kết quả của mọi Promise dù có cái nào fail.

Tổng kết

Sau chương 6, bạn nên đã master:

  • Event loop: Call Stack + Web/Node API + Macrotask Q + Microtask Q. Rule "stack rỗng → drain micro → 1 macro → loop".
  • Callback hell + IoC: lý do Promise sinh ra.
  • Promise 3 trạng thái: Pending → Fulfilled/Rejected (không đảo ngược).
  • Chaining: .then trả Promise mới, return value/Promise đều OK, error propagate đến .catch gần nhất.
  • 4 combinators: all (tất cả), allSettled (biết hết), race (cái nhanh nhất), any (cái resolve đầu).
  • async/await: sugar trên Promise. async function luôn trả Promise. await tạm dừng function, không block thread.
  • Sequential vs parallel: dùng Promise.all(arr.map(fn)) khi không phụ thuộc nhau.
  • Error handling: try/catch quanh await, .catch cho chain. Lỗi trong setTimeout KHÔNG bị bắt bởi outer try/catch.
  • AbortController: standard cancel cho fetch và các API async.
  • async ≠ parallel: CPU-bound vẫn block, cần Web Worker / Worker Threads.

Kết nối

  • Chương 5 (Arrays, Iterables) — async iterable, for await...of, stream xử lý qua iterator protocol.
  • Chương 7 (Error Handling) — Result type pattern, custom Error class, error boundary cho async chain.
  • Dart Chương 7 (Async) — đối chiếu Future/async/await Dart với JS Promise. Dart có cùng mental model nhưng Future có thể được "completed" nhiều lần qua Completer.
  • Flutter Chương 10 (Network) — áp dụng async để gọi REST API, xử lý loading/error state, cancellation với http.Client tương tự AbortController.
  • Networking (Pillar 4) — TCP/HTTP là I/O delegated ra Node/browser, hiểu protocol giúp debug fetch chính xác.