Menu

async/await trong JavaScript: code bất đồng bộ dễ đọc

Cách async/await thật sự hoạt động trong JavaScript: hàm async, chờ promise, xử lý lỗi bằng try/catch và chạy các tác vụ song song.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

async/await chỉ là promise được ngụy trang

async/await không phải một mô hình xử lý đồng thời mới. Nó là cú pháp tiện lợi xây trên promise, cho phép bạn viết code trông tuần tự dù thực chất là bất đồng bộ. Cùng một cơ chế, nhưng hình dạng thân thiện hơn nhiều.

Đây là cùng một tác vụ viết theo hai cách:

Cả hai hàm đều trả về promise. Cả hai làm đúng một việc như nhau. Phiên bản async đọc từ trên xuống dưới, không có chuỗi .then, và đó là toàn bộ sức hấp dẫn của nó.

async đánh dấu một hàm trả về promise

Đặt async trước một function, một arrow function hoặc một phương thức, và hai điều sẽ xảy ra:

  1. Hàm luôn trả về một promise. Bất cứ thứ gì bạn return sẽ trở thành giá trị được resolve.
  2. Bạn được phép dùng await bên trong nó.

Để ý rằng result không phải là chuỗi, mà là một promise sẽ resolve thành chuỗi đó. Dù greet không chứa await nào và không làm việc bất đồng bộ nào, từ khóa async vẫn bọc giá trị trả về trong một promise. Nếu hàm ném lỗi, promise sẽ bị reject.

await tạm dừng cho đến khi promise hoàn tất

Bên trong một hàm async, await somePromise tạm dừng hàm đó cho đến khi promise resolve, rồi đưa cho bạn giá trị đã resolve. Nếu promise bị reject, await sẽ ném lỗi.

Hãy để ý thứ tự đầu ra. "started countdown" được in trước "2", vì await chỉ tạm dừng hàm async chứ không dừng phần còn lại của chương trình. Event loop vẫn chạy; countdown chỉ tiếp tục sau đó, khi từng promise wait resolve.

Bạn có thể await bất cứ thứ gì giống promise. await 42 cũng hợp lệ: giá trị không phải promise sẽ được bọc trong Promise.resolve(42) và resolve ngay lập tức.

Xử lý lỗi với try/catch

Với promise thông thường, bạn nối .catch(). Với async/await, một promise bị reject trở thành một ngoại lệ được ném ra mà bạn bắt theo cách bình thường:

Một khối try/catch bao được mọi await bên trong nó. Lỗi mạng, lỗi phân tích JSON và các câu lệnh throw của chính bạn đều rơi vào cùng một catch. Đó là một bước tiến thật sự so với các chuỗi .then/.catch lồng nhau.

Một điều cần để ý: fetch chỉ reject khi có lỗi mạng, không reject với HTTP 4xx/5xx. Bạn phải tự kiểm tra res.ok rồi ném lỗi, một khuôn mẫu bạn sẽ gặp liên tục trong code thực tế.

Đừng await trong vòng lặp khi không cần

Đây là cái bẫy phổ biến nhất của async/await. await tuần tự bên trong vòng lặp nghĩa là mỗi lần lặp phải chờ lần lặp trước:

sequential mất khoảng 900ms. parallel mất khoảng 300ms. Quy tắc chung: nếu các tác vụ không phụ thuộc vào kết quả của nhau, hãy khởi động tất cả rồi await Promise.all. Chỉ await từng cái một khi lời gọi tiếp theo thật sự cần kết quả của lời gọi trước.

Với một tập hợp, Promise.all(items.map(async (x) => ...)) là cách viết chuẩn. Một vòng lặp for...of có await bên trong sẽ chạy tuần tự: đôi khi bạn muốn vậy (giới hạn tốc độ, giữ thứ tự) nhưng thường thì không.

Kết hợp async/await với promise thông thường

Bạn không cần chọn phe. Hàm async trả về promise, và await dùng được với mọi promise, nên bạn có thể kết hợp chúng thoải mái:

Hai kiểu viết có thể thay thế cho nhau. Dùng await khi code đọc từ trên xuống dễ hiểu hơn; dùng .then khi bạn cần viết nhanh một lần hoặc đang ở ngoài ngữ cảnh async.

Top-level await (trong ES module)

Trước đây, bạn phải bọc await trong một hàm async vì await không được phép ở cấp ngoài cùng của script. Điều đó đã thay đổi: bên trong một ES module (tệp .mjs hoặc <script type="module">), giờ bạn có thể await trực tiếp ở cấp ngoài cùng:

// in an ES module
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await res.json();
console.log(user.name);

Top-level await hoãn việc hoàn tất module cho đến khi promise được chờ hoàn tất, và mọi nơi import module đó cũng phải chờ theo. Tiện cho việc tải cấu hình và import động, nhưng hãy dùng tiết chế: một top-level await chậm sẽ chặn mọi nơi import module đó.

Trong tệp CommonJS hoặc script nhúng thông thường, cách này vẫn lỗi với SyntaxError. Cách lách cổ điển là một hàm async được gọi ngay lập tức:

Những chỗ hay vấp

Điểm nhanh qua các cái bẫy quen thuộc:

  • Quên async. Dùng await trong hàm thường là lỗi cú pháp. Cách sửa là thêm async, hoặc gọi hàm async hỗ trợ bằng .then.
  • Quên await kết quả. const data = getJSON(url); cho bạn một promise, không phải dữ liệu. Nếu bạn dùng nó như thể là giá trị, bạn sẽ thấy [object Promise] xuất hiện trong đầu ra.
  • Reject không được xử lý. Một hàm async bạn gọi rồi bỏ đó (doWork();) sẽ âm thầm nuốt lỗi, trừ khi bạn thêm .catch hoặc await nó bên trong try/catch.
  • forEach với callback async. array.forEach(async (x) => await something(x)) không chờ gì cả: forEach bỏ qua các promise được trả về. Hãy dùng for...of với await, hoặc Promise.all(array.map(...)).

Chạy thử xem: "finished?" được in trước mọi "done", vì broken trả về mà không chờ. fixed chờ mọi thứ xong rồi mới in "finished!" ở cuối cùng.

Khi nào nên dùng async/await

Mặc định dùng async/await cho mọi đoạn code có nhiều hơn một bước bất đồng bộ nối tiếp nhau, hoặc cần xử lý lỗi kiểu try/catch. Giữ promise thuần cho những dòng code đơn giản, cho code thư viện trả về promise mà bản thân không cần chờ gì, hoặc khi bạn thật sự cần các hàm kết hợp như Promise.race hay .finally() trong một chuỗi.

Dùng đúng cách, async/await khiến code bất đồng bộ đọc như một công thức nấu ăn: làm việc này, rồi việc này, rồi việc này. Event loop vẫn làm việc của nó, chỉ là bạn không còn phải tư duy bằng callback nữa.

Tiếp theo: fetch API

Phần lớn ví dụ ở đây dùng fetch làm đại diện cho "một việc bất đồng bộ nào đó". Nó đáng được xem xét kỹ: request và response hoạt động thế nào, xử lý JSON, đặt header, và vì sao fetch không reject khi gặp lỗi HTTP. Đó là nội dung của trang tiếp theo.

Câu hỏi thường gặp

async/await làm gì trong JavaScript?

async/await là cú pháp làm việc với promise, cho phép bạn viết code bất đồng bộ như thể nó là code đồng bộ. async đánh dấu một hàm trả về promise, còn await tạm dừng bên trong hàm đó cho đến khi một promise hoàn tất, rồi đưa cho bạn giá trị đã resolve. Bên dưới, nó vẫn là promise, chỉ là dễ đọc hơn.

Có thể dùng await bên ngoài hàm async không?

Ở cấp ngoài cùng của một ES module thì được, đó gọi là top-level await. Bên trong hàm thường hoặc trong script CommonJS thì không: await bên ngoài hàm async là lỗi cú pháp. Cách sửa thường là bọc code trong một hàm async rồi gọi nó, hoặc chuyển tệp sang ES module.

Làm sao để xử lý lỗi với async/await?

Bọc các lời gọi có await trong try/catch. Mọi promise bị reject mà bạn await sẽ biến thành một ngoại lệ được ném ra để khối catch xử lý. Với các tác vụ chạy nền mà bạn không await, hãy gắn .catch() vào promise trả về để lỗi reject không bị bỏ sót.

await có chặn toàn bộ chương trình không?

Không. await chỉ tạm dừng hàm async hiện tại. Event loop vẫn tiếp tục chạy: timer vẫn kích hoạt, các tác vụ bất đồng bộ khác vẫn tiến triển, giao diện vẫn phản hồi. Code gọi hàm nhận lại một promise đang chờ và chạy tiếp ngay lập tức.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu