- 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?
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):
- Chờ Call Stack rỗng.
- 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.
- Lấy đúng 1 macrotask từ macrotask queue, push vào stack, chạy đến khi stack rỗng.
- Render UI (browser) nếu cần.
- Lặp lại từ bước 1.
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:
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 |
|---|---|---|---|---|---|
| 1 | main, console.log('A') | — | — | — | A |
| 2 | main, setTimeout(...) | Timer 0ms | — | — | — |
| 3 | main, Promise.then(...) | Timer 0ms | — | cb-C | — |
| 4 | main, console.log('D') | Timer 0ms expire → push | cb-B | cb-C | D |
| 5 | rỗng | — | cb-B | cb-C | — |
| 6 | cb-C (drain micro) | — | cb-B | rỗng | C |
| 7 | rỗng | — | cb-B | rỗng | — |
| 8 | cb-B (1 macro) | — | rỗng | rỗng | B |
Kết quả output: A, D, C, B.
"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.
// 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":
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
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ĩa | Có thể chuyển sang |
|---|---|---|
| Pending | Mới tạo, chưa xong | Fulfilled hoặc Rejected |
| Fulfilled | Hoàn thành thành công với value | Không thể chuyển |
| Rejected | Thất bại với reason | Khô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
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
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'));
.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.
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
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.
// 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"
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"
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.race và Promise.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);
}
- 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:
async functionluôn trả về một Promise. Return value bình thường → Promise resolve. Throw → Promise reject.awaitchỉ dùng trongasync function(hoặc top-level module).await promisetạm dừng function, đợi Promise settle, rồi tiếp tục.
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 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.
// 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];
}
// 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:
async function main() {
try {
const data = await fetchData();
return data;
} catch (err) {
console.error('Lỗi:', err);
return null;
} finally {
cleanup();
}
}
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 setTimeouttry {
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.
// 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.
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);
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ạ:
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:
- Đồng bộ chạy hết:
1, 7. - Drain micro queue. Queue có
[then-3, then-6]. Chạy3— schedule thêmthen-4vào queue. Tiếp tục drain:6, rồi4. - Pick 1 macrotask:
2. Drain micro (rỗng). Pick tiếp:5.
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.
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.
// 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: 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
Output của:
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
Xem đáp án
1, 4, 3, 2. Sync chạy trước (1, 4). Microtask (then) drain trước macrotask (setTimeout) → 3 rồi 2.
async function foo() { return 1; } — gọi foo() trả gì?
Xem đá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(...).
try { setTimeout(() => { throw new Error('x'); }, 0) } catch (e) { ... } — catch bắt được lỗi không?
Xem đá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.
await Promise.all([]) trả gì?
Xem đá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.
await Promise.race([]) trả gì?
Xem đá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.
for await (const x of asyncIterable) có hoạt động không? Khi nào dùng?
Xem đáp án
Có. 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.
Gọi async function có tự động chạy song song nhau không?
Xem đá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ự.
Promise.allSettled giải quyết hạn chế gì của Promise.all?
Xem đá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:
.thentrả Promise mới, return value/Promise đều OK, error propagate đến.catchgầ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.
awaittạ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/catchquanhawait,.catchcho chain. Lỗi trongsetTimeoutKHÔ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/awaitDart với JS Promise. Dart có cùng mental model nhưngFuturecó thể được "completed" nhiều lần quaCompleter. - Flutter Chương 10 (Network) — áp dụng async để gọi REST API, xử lý loading/error state, cancellation với
http.Clienttươ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.