Menu

Async await trong TypeScript: kiểu, lỗi và gọi song song

Cách async và await được khai báo kiểu trong TypeScript: hàm async trả về Promise<T>, await mở nó ra, lỗi được bắt bằng try/catch, top-level await cần ES module, cùng với khác biệt giữa await lần lượt, await song song và cái bẫy forEach.

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.

Một hàm async trong TypeScript luôn trả về promise: nếu thân hàm trả về string, kiểu của nó là Promise<string>. Bên trong hàm, await mở một Promise<T> thành T.

Hành vi lúc runtime (await tạm dừng cái gì, event loop) là của JavaScript, được trình bày ở async/await trong JavaScript. Phần của TypeScript là kiểu dữ liệu đi vào và đi ra.

Kiểu trả về của hàm async

Kiểu trả về được khai báo của hàm async phải là Promise<...>, dù thân hàm trả về giá trị thường. TypeScript sẽ tự suy luận nếu bạn không ghi.

Viết async function count(): number gây lỗi TS1064 (The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<number>'?). Utility type Awaited<T> cho kiểu đã được mở: Awaited<ReturnType<typeof count>> là number.

Quên await

Thiếu await khiến bạn cầm trong tay một Promise<T> thay vì T. TypeScript bắt được phần lớn trường hợp này vì các kiểu không còn khớp:

Lỗi if (ok) là có thật: một đối tượng promise luôn là truthy, nên nó đã cấp quyền truy cập. TypeScript báo đó là TS2801. Gán promise cho một biến boolean, hoặc đọc một thuộc tính mà promise không có, cũng sẽ không biên dịch được. Một lời gọi mà bạn bỏ qua kết quả (save(user);) thì không bị bắt; quy tắc no-floating-promises của typescript-eslint lo việc đó.

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

Một promise bị reject khiến await ném lỗi, nên try/catch thông thường vẫn dùng được. Với strict, biến trong catch có kiểu unknown, và bạn thu hẹp nó trước khi đọc .message:

throw bên trong hàm async sẽ reject promise của nó thay vì ném lỗi tại chỗ gọi. Các mẫu khác, gồm lớp lỗi tùy chỉnh và trả về kết quả thay vì ném lỗi, có ở trang xử lý lỗi.

Top-level await

await bên ngoài mọi hàm chỉ chạy được trong ES module. Một tệp được biên dịch thành CommonJS (trường hợp của các ví dụ này, và của các dự án Node không có "type": "module") sẽ từ chối nó:

index.ts(2,14): error TS1309: The current file is a CommonJS module and cannot use 'await' at the top level.

Bọc code trong một hàm async main rồi gọi nó, như mọi ví dụ trên trang này. Trong dự án ES module ("type": "module" trong package.json với module đặt là node16 hoặc nodenext, hoặc module: "esnext" cho bundler), top-level await được cho phép.

Tuần tự và song song

Mỗi await chờ promise của nó xong rồi dòng tiếp theo mới bắt đầu. Với các lời gọi độc lập, hãy khởi chạy tất cả trước rồi await chúng cùng lúc bằng Promise.all:

Ở nửa tuần tự, b không thể xong trước a vì nó chưa bắt đầu. Ở nửa song song, d xong trước, và tổng thời gian xấp xỉ lời gọi lâu nhất thay vì tổng của tất cả. Promise.all vẫn trả về kết quả theo thứ tự đầu vào, với kiểu là một tuple.

Cái bẫy forEach

forEach bỏ qua promise mà callback async trả về, nên không có gì chờ công việc hoàn tất:

for...of với await xử lý từng phần tử một; Promise.all với map chạy chúng song song và chờ tất cả. forEach không làm được cả hai, và TypeScript không cảnh báo, vì một callback có kiểu trả về void vẫn chấp nhận callback trả về promise.

Lặp bất đồng bộ với for await

for await...of lặp qua một async iterable, chẳng hạn async generator, và await từng giá trị:

async function* pages(total: number): AsyncGenerator<string[]> {
    for (let page = 1; page <= total; page++) {
        await new Promise((r) => setTimeout(r, 10));
        yield [`item ${page}a`, `item ${page}b`];
    }
}

async function main() {
    for await (const batch of pages(3)) {
        console.log(batch.join(", ")); // batch: string[]
    }
}
main();

Kiểu phần tử lấy từ chú thích AsyncGenerator<T> của generator, hoặc được suy luận khi bạn không ghi.

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

Hàm async trong TypeScript có kiểu trả về là gì?

Luôn là một promise. Hàm async trả về number có kiểu trả về Promise<number>, còn hàm không trả về gì có kiểu Promise<void>. Viết async function f(): number gây lỗi TS1064, kèm gợi ý dùng Promise<number>.

Dùng await ở cấp cao nhất trong TypeScript thế nào?

Top-level await chỉ chạy trong ES module, với module đặt là es2022, esnext, system, preserve, hoặc node16/node18/node20/nodenext trong một tệp mà Node coi là ESM, và target từ es2017 trở lên. Trong tệp CommonJS, nó gây lỗi TS1309. Cách sửa dùng được ở mọi nơi là một hàm async main: async function main() { ... } main();.

Xử lý lỗi với async/await trong TypeScript thế nào?

Bọc await trong try/catch. Với strict, giá trị bắt được có kiểu unknown, nên hãy thu hẹp nó trước: if (e instanceof Error) console.log(e.message). Một promise bị reject mà không bao giờ được await hay catch sẽ trở thành unhandled rejection.

Chạy các lời gọi async song song trong TypeScript thế nào?

Khởi chạy tất cả promise trước, rồi await chúng cùng lúc: const [a, b] = await Promise.all([loadA(), loadB()]). Viết await loadA(); await loadB(); sẽ chạy lần lượt từng cái. Promise.all giữ nguyên kiểu của từng kết quả trong tuple trả về.

Vì sao async không hoạt động bên trong forEach?

forEach gọi callback và bỏ qua giá trị nó trả về, nên các promise từ callback async không bao giờ được await: vòng lặp kết thúc ngay và code phía sau chạy trước khi công việc xong. Dùng for...of với await để xử lý lần lượt, hoặc await Promise.all(items.map(async (x) => ...)) để xử lý song song.

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