Callback, Promise và async/await: cùng một concurrency model, khác cách viết

JavaScript không biến thành multi-threaded chỉ vì có async. Trên browser, event loop phối hợp call stack, task queues và Web APIs để callback tiếp tục chạy khi công việc bất đồng bộ hoàn tất.

Callback

Callback là function được truyền để gọi sau:

1
2
3
function loadUser(id, callback) {
setTimeout(() => callback(null, { id, name: 'Hudson' }), 100);
}

Node-style callback thường dùng (error, value). Callback không mặc định bất đồng bộ; Array#map cũng nhận callback nhưng chạy đồng bộ.

Nhiều tầng callback làm control flow và error handling khó đọc, nhưng vấn đề không phải callback “xấu”; vấn đề là composition yếu.

Promise

Promise đại diện cho kết quả sẽ fulfilled hoặc rejected:

1
2
3
4
5
6
7
8
9
10
11
function loadUser(id) {
return fetch(`/api/users/${id}`).then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
});
}

loadUser(1)
.then(user => loadOrders(user.id))
.then(renderOrders)
.catch(showError);

Luôn return promise trong .then; nếu quên, chain tiếp theo không chờ công việc đó.

async/await

async function luôn trả Promise. await tạm dừng function đó, không block toàn bộ thread:

1
2
3
4
5
6
7
8
9
async function renderUserOrders(userId) {
try {
const user = await loadUser(userId);
const orders = await loadOrders(user.id);
renderOrders(orders);
} catch (error) {
showError(error);
}
}

async/await là syntax trên Promise, không phải concurrency engine mới.

Chạy tuần tự hay song song

1
2
3
// Tuần tự
const user = await loadUser(id);
const orders = await loadOrders(user.id);

Request thứ hai phụ thuộc request đầu nên tuần tự là đúng.

1
2
3
4
5
// Song song
const [cats, dogs] = await Promise.all([
loadCats(),
loadDogs(),
]);

Dùng Promise.allSettled khi muốn thu kết quả từng task dù một task fail.

Microtask và task

Promise reactions chạy trong microtask queue, thường được drain sau stack hiện tại và trước task tiếp theo như timer:

1
2
3
4
5
6
console.log('A');
setTimeout(() => console.log('timer'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('B');

// A, B, promise, timer

Đây là thứ tự queue, không phải Promise “nhanh hơn” timer theo nghĩa performance.

Hủy và timeout

Promise tự nó không có nút cancel chung. API như Fetch dùng AbortController:

1
2
3
4
5
6
7
8
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5_000);

try {
await fetch('/api/report', { signal: controller.signal });
} finally {
clearTimeout(timeout);
}

Quy tắc thực dụng

  • Dùng callback cho event/subscription.
  • Dùng Promise cho một kết quả bất đồng bộ.
  • Dùng async/await để control flow dễ đọc.
  • Chạy song song chỉ khi task độc lập.
  • Luôn xử lý rejection và cleanup.

Syntax có thể đẹp hơn, nhưng lỗi race condition vẫn xấu như cũ. Nó chỉ mặc áo mới.

References