Danh sách bài viết

Bài 241: async fn Syntax

Bài 241 của series Rust Cơ Bản — async fn là cú pháp khai báo một asynchronous function trong Rust, chỉ cần thêm keyword async trước fn là xong: async fn foo() -> i32 { 42 }. Trông y hệt một sync function, nhưng bản chất hoàn toàn khác: gọi foo() không chạy body và trả về i32; nó trả về một giá trị thuộc kiểu impl Future<Output = i32> — một state machine mà compiler sinh ra, chứa toàn bộ logic chờ để chạy sau. Muốn lấy được i32 thực sự, caller phải dùng toán tử .await trong một async context khác. Đây cũng là điểm gây bối rối cho người mới: gọi foo(); mà quên .await sẽ không chạy gì cả — và may mắn là Future được đánh dấu #[must_use], compiler warn ngay "unused implementer of Future that must be used". Về desugaring, async fn foo() -> T tương đương fn foo() -> impl Future<Output = T> — hiểu được tương đương này là chìa khoá để đọc signature thư viện async, đặc biệt khi gặp lifetime trong async function. Cuối cùng, từ Rust 1.75 (12/2023), AFIT (async fn in traits) đã ổn định: bạn có thể viết thẳng async fn trong trait mà không cần macro #[async_trait] như trước. Bài này tập trung vào cú pháp và bản chất; chi tiết Future trait và .await sẽ được học sâu ở các bài tiếp theo trong nhóm.

10/06/2026
9 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Viết được async fn đơn giản với cú pháp đúng và hiểu khác biệt so với fn thường ở chỗ nào.
  • Hiểu bản chất của async fn: gọi nó không chạy body — nó trả về một Future (state machine) mà runtime phải drive.
  • Đọc được desugaring async fn foo() -> Tfn foo() -> impl Future<Output = T> và biết khi nào hai dạng tương đương, khi nào lệch về lifetime.
  • Biết bắt buộc dùng .await trong async context để lấy giá trị thực sự, và không thể .await trong sync function.
  • Nhận diện được warning "unused Future that must be used" khi quên .await — và biết đây là vì Future có attribute #[must_use].
  • Khai báo được async fn trong impl block của struct và trong trait (nhờ AFIT ổn định từ Rust 1.75).
  • Biết hạn chế hiện tại của AFIT (chưa hỗ trợ trait object dyn Trait trực tiếp) và workaround #[async_trait] khi cần dynamic dispatch.
2

async fn — Cú Pháp Cơ Bản

Cú pháp đơn giản đến mức gây hiểu lầm: chỉ thêm keyword async trước fn:

// Sync function thường
fn foo() -> i32 {
    42
}

// Async function — chỉ khác chữ `async`
async fn bar() -> i32 {
    42
}

Mọi thứ còn lại — tên hàm, parameter, return type, body — viết y hệt. Body có thể chứa code Rust bình thường: tính toán, gọi function khác, dùng let, if, match, ?. Có thể nhận parameter, có thể trả về Result, có thể là pub:

pub async fn fetch_user(id: u64) -> Result<String, std::io::Error> {
    // pretend ta đọc file hoặc gọi HTTP ở đây
    Ok(format!("user {id}"))
}

Một điểm nhỏ nhưng quan trọng: fn main() mặc định không được async — entry point của binary là sync. Muốn có async fn main, bạn phải dùng macro của runtime, ví dụ #[tokio::main] ở các bài sau. Còn lúc này, ta sẽ viết các async fn nhỏ và chạy chúng từ một sync main qua runtime.

3

Bản Chất: async fn Trả Về Future

Đây là điểm hoàn toàn khác với sync function và là chỗ nhiều người mới vấp. Nhìn signature async fn bar() -> i32, bạn rất dễ tưởng bar() trả về i32. Sự thật: gọi bar() không chạy body và không trả về i32. Nó trả về một giá trị mới thuộc một kiểu ẩn danh implement trait Future<Output = i32> — một state machine mà compiler tự sinh, chứa toàn bộ code body của bar đóng băng lại, chờ ai đó drive nó.

async fn bar() -> i32 { 42 }

fn main() {
    let fut = bar();    // KHÔNG in gì cả; body của bar() chưa chạy.
    // `fut` có kiểu `impl Future<Output = i32>` — chưa phải i32.
    drop(fut);          // drop future = vứt state machine, không có side effect.
}

Đặc tính này gọi là lazy: future trong Rust không chạy cho đến khi được poll. Trái ngược với JavaScript Promise hay C# Taskeager bắt đầu chạy ngay khi tạo. Lazy có ưu thế: cho phép cancel "miễn phí" (drop future = huỷ), không tốn resource cho future không được dùng, và compiler có thể fuse nhiều future thành state machine duy nhất.

Hệ quả thực dụng: chỉ tạo future không đủ để code chạy. Phải có .await hoặc trao future cho một executor (như tokio::spawn). Đây là khác biệt nền tảng cần ghi nhớ trước khi đi tiếp.

4

Desugaring: fn -> impl Future<Output = T>

Compiler thực chất viết lại async fn thành một sync fn trả về impl Future. Hai khai báo dưới là tương đương về mặt ngữ nghĩa:

// Dạng async fn (cú pháp đường)
async fn fetch(id: u64) -> String {
    format!("user {id}")
}

// Dạng desugar tương đương
fn fetch(id: u64) -> impl std::future::Future<Output = String> {
    async move {
        format!("user {id}")
    }
}

Hai cách viết này có cùng signature ở góc nhìn của caller — đều trả về thứ implement Future<Output = String>, đều cần .await để lấy String ra. Trong code thực, gần như ai cũng dùng dạng async fn vì gọn hơn; dạng desugar chỉ xuất hiện khi bạn cần kiểm soát chi tiết hoặc khi đọc tài liệu của compiler.

Một số trường hợp không hoàn toàn tương đương là khi có lifetime dính vào parameter reference:

// async fn — lifetime của tham chiếu tự động bind vào future trả về
async fn read_first(buf: &[u8]) -> u8 {
    buf[0]
}

// Desugar đầy đủ (Rust ngầm hiểu)
fn read_first<'a>(buf: &'a [u8]) -> impl std::future::Future<Output = u8> + 'a {
    async move { buf[0] }
}

Điểm chốt: future trả về vẫn còn giữ tham chiếu &[u8] cho đến khi nó .await xong, nên future phải có lifetime 'a. Với async fn, compiler tự ngầm thêm; với dạng desugar viết tay, bạn phải khai báo rõ + 'a. Đây là lý do bạn sẽ thấy + 'a nhan nhản trong signature lib async.

5

Gọi async fn Phải .await

Để biến impl Future<Output = T> thành T, bạn dùng toán tử hậu tố .await. Toán tử này chỉ dùng được trong async context — nghĩa là bên trong một async fn hoặc một async { ... } block khác.

async fn bar() -> i32 { 42 }

async fn caller() -> i32 {
    let x: i32 = bar().await; // .await chạy bar() và chờ kết quả
    x + 1
}

.await nói với runtime: "tôi cần kết quả của future này; nếu nó chưa sẵn sàng, hãy tạm dừng tôi và làm việc khác". Khi future ready, runtime đánh thức và tiếp tục đoạn code phía sau. Đây là cách Rust làm cooperative concurrency trong một thread duy nhất, không cần thread mới cho mỗi tác vụ.

Trong sync context, .awaitcompile error:

fn main() {
    let x = bar().await; // error[E0728]: `await` is only allowed inside `async` functions and blocks
}

Để chạy một future từ sync main, bạn cần một runtime — thường là tokio. Mẫu phổ biến nhất:

// Cargo.toml: tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
    let x = bar().await;
    println!("{x}"); // 42
}

Macro #[tokio::main] tự sinh sync main bên trong, tạo runtime, rồi gọi runtime.block_on(async_body). Chi tiết runtime sẽ ở các bài 246+, ở đây chỉ cần biết "muốn chạy async, phải có runtime".

6

Lỗi Phổ Biến: Quên .await

Vì cú pháp async fn trông giống sync fn, người mới rất hay viết:

async fn save(name: &str) {
    // pretend ghi DB
}

#[tokio::main]
async fn main() {
    save("an");  // QUÊN .await — không ghi gì cả!
}

Code biên dịch được, nhưng save không bao giờ chạy. Bạn chỉ tạo ra một future rồi vứt đi ngay (lazy + drop = no-op). Đây là class bug rất khó debug nếu không quen.

May mắn, trait Future trong stdlib có attribute #[must_use = "futures do nothing unless you `.await` or poll them"]. Compiler sẽ warn ngay lập tức:

warning: unused implementer of `Future` that must be used
 --> src/main.rs:7:5
  |
7 |     save("an");
  |     ^^^^^^^^^^
  |
  = note: futures do nothing unless you `.await` or poll them
  = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
  |
7 |     let _ = save("an");
  |     +++++++

Đừng silence warning này bằng let _ = save("an"); như compiler đề xuất — đó là cách tắt warning chứ không phải fix. Fix đúng là thêm .await (chờ chạy xong) hoặc đẩy vào executor (tokio::spawn(save("an")) để chạy concurrent). Tip: bật #![deny(unused_must_use)] ở crate root để biến warning này thành compile error — code async sẽ an toàn hơn nhiều.

Một biến thể khác cũng hay gặp: gọi .await rồi nhưng bên trong sync closure (ví dụ .map(|x| async_op(x).await)) — closure đó không phải async block, cũng báo lỗi. Sửa thành .then(|x| async_op(x)) hoặc dùng async closure.

7

async fn Trong impl Block Và Trait

async fn hoạt động bình thường trong impl block của struct. Cú pháp giống hệt:

struct UserRepo {
    base_url: String,
}

impl UserRepo {
    async fn find(&self, id: u64) -> Option<String> {
        // pretend HTTP GET {base_url}/users/{id}
        Some(format!("user {id} @ {}", self.base_url))
    }
}

#[tokio::main]
async fn main() {
    let repo = UserRepo { base_url: "http://api".into() };
    if let Some(u) = repo.find(7).await {
        println!("{u}");
    }
}

Lưu ý &self: future trả về sẽ giữ tham chiếu &UserRepo đến khi .await xong — repo không được drop trong khoảng đó. Borrow checker tự xử lý.

Trong trait, mọi chuyện từng rắc rối hơn rất nhiều — trước Rust 1.75, viết async fn trong trait không compile, phải dùng macro #[async_trait] của crate async-trait để rewrite thành fn -> Pin<Box<dyn Future>>. Từ Rust 1.75 (12/2023), tính năng AFIT (async fn in traits) đã ổn định, bạn viết thẳng được:

trait Repo {
    async fn find(&self, id: u64) -> Option<String>;
}

struct InMemRepo;

impl Repo for InMemRepo {
    async fn find(&self, id: u64) -> Option<String> {
        Some(format!("in-mem user {id}"))
    }
}

async fn use_repo<R: Repo>(repo: &R) {
    if let Some(u) = repo.find(42).await {
        println!("{u}");
    }
}

Hạn chế: AFIT hiện chưa hỗ trợ trực tiếp dyn Trait (trait object) — không thể có Box<dyn Repo> với async fn trong trait. Lý do: future trả về có kiểu ẩn danh, không cố định kích thước, không gắn vào vtable thẳng được. Hai workaround chính:

  • Generic bound (fn use_repo<R: Repo>(repo: &R)) như ví dụ trên — static dispatch, nhanh, không cần dyn.
  • Khi bắt buộc phải có dyn Repo (lưu nhiều implementation khác nhau trong một Vec), vẫn dùng macro #[async_trait::async_trait] như trước — nó rewrite thành fn -> Pin<Box<dyn Future + Send>> tương thích với dyn.

Tin vui: cả hai cách đều phổ biến và đều idiomatic ở thời điểm 2026. Crate async-trait vẫn được maintain tốt và sẽ tồn tại cho đến khi RFC dyn-compatible async-fn-in-trait được hoàn thiện.

8

Tổng Kết

  • async fn: chỉ thêm keyword async trước fn là xong. Body viết code Rust bình thường, có ?, match, let... như sync.
  • Bản chất: gọi async fn không chạy body. Nó trả về một giá trị thuộc kiểu ẩn danh implement Future<Output = T> — state machine compiler-generated, lazy, chờ ai đó drive.
  • Desugaring: async fn foo() -> Tfn foo() -> impl Future<Output = T>. Khi parameter có reference, lifetime tự động bind vào future trả về (+ 'a).
  • Phải .await để lấy giá trị thực sự. .await chỉ dùng được trong async context (async fn hoặc async block). Sync context cần runtime (#[tokio::main], block_on).
  • Quên .await: body không chạy. Compiler warn nhờ #[must_use] trên Future. Tip: bật #![deny(unused_must_use)] để biến warning thành error.
  • Trong impl block: cú pháp y hệt, &self/&mut self hoạt động bình thường, lifetime tự xử lý.
  • Trong trait: từ Rust 1.75 (12/2023), AFIT ổn định — viết thẳng async fn trong trait không cần macro. Hạn chế: chưa hỗ trợ dyn Trait; cần dynamic dispatch thì dùng #[async_trait::async_trait] như cũ.
  • Lazy semantics: future Rust khác Promise JS/Task C# — không tự chạy khi tạo. Ưu điểm: cancel miễn phí (drop = huỷ), không waste resource, compiler optimize tốt hơn.
9

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Viết async fn add(a: i32, b: i32) -> i32 trả về tổng. Sau đó viết async fn main_logic() gọi add(2, 3) và in kết quả. Vì sao gọi không có .await sẽ không in gì?
  2. Cho async fn ping() -> bool { true }. Viết dạng desugar tương đương dùng fn -> impl Future<Output = bool>async { ... } block.
  3. Đoạn code let x = ping().await; đặt trong fn main() (sync) báo lỗi gì? Cách sửa ngắn gọn nhất?
  4. Vì sao Rust chọn future lazy (không tự chạy) thay vì eager như JavaScript Promise? Nêu ít nhất hai lợi ích.
  5. Trước Rust 1.75, làm thế nào để có async fn trong trait? Sau Rust 1.75 thì khác gì, và hạn chế gì còn lại?
  6. Viết một trait Cache có method async fn get(&self, key: &str) -> Option<String>, sau đó implement cho struct MemCache trả về Some(key.to_string()).
Đáp án
  1. async fn add(a: i32, b: i32) -> i32 { a + b }. async fn main_logic() { let s = add(2, 3).await; println!("{s}"); }. Gọi không .await chỉ tạo future (state machine) rồi drop ngay; body của add chưa chạy lần nào, không có side effect nào xảy ra — Rust future là lazy.
  2. fn ping() -> impl std::future::Future<Output = bool> { async { true } }. Tương đương ngữ nghĩa với async fn ping() -> bool { true }.
  3. Báo lỗi error[E0728]: \`await\` is only allowed inside \`async\` functions and blocks. Sửa ngắn nhất: thêm #[tokio::main] và đổi fn main thành async fn main (cần dependency tokio với feature full hoặc rt-multi-thread + macros).
  4. (a) Cancel miễn phí: drop future = huỷ tác vụ, không cần cơ chế cancel token phức tạp. (b) Không waste resource: future chưa .await không tốn gì. (c) Compiler có thể fuse nhiều future thành một state machine duy nhất, tối ưu allocation. (d) Composability: dễ build combinator như select!, join! trên top.
  5. Trước 1.75: phải dùng macro #[async_trait::async_trait] của crate async-trait — nó rewrite async fn thành fn -> Pin<Box<dyn Future + Send + 'a>>. Từ 1.75: viết thẳng async fn trong trait (AFIT). Hạn chế còn lại: chưa hỗ trợ trực tiếp dyn Trait (trait object); muốn Box<dyn MyTrait> vẫn cần #[async_trait].
  6. trait Cache { async fn get(&self, key: &str) -> Option<String>; } struct MemCache; impl Cache for MemCache { async fn get(&self, key: &str) -> Option<String> { Some(key.to_string()) } }. Dùng được trong generic context fn run<C: Cache>(c: &C); muốn Box<dyn Cache> cần #[async_trait].
10

Bài Tiếp Theo

Bài 242: async Block — async { ... } — bài tiếp khám phá async { ... } block: cách tạo một future inline ngay tại chỗ mà không cần khai báo function riêng, sự khác biệt giữa async block và async move block (capture by reference vs by value), cách kết hợp nhiều async block trong cùng scope, và use case xây future tùy biến hoặc tạo future ngắn để pass vào tokio::spawn. Sau đó bài 243 sẽ mổ xẻ chính trait Future: poll, Pin, Waker — bản chất state machine mà compiler sinh ra cho mọi async fn.