# Lesson: async fn in traits vs dyn-compatibility ## The problem The `HookHandler` trait needs to be stored as `Arc` so the menu engine can hold a type-erased handler without knowing whether it's an exec hook, a handler hook, or a composite. But `async fn` in traits (even with edition 2024's native support) makes the trait not dyn-compatible. The compiler can't build a vtable for an async method because the return type (an opaque `impl Future`) varies per implementation. ## What we tried Official documentation of edition 2024 supports async fn in traits natively, async-trait is NOT needed." That's true for static dispatch (generics), but wrong for `dyn` dispatch. ## The fix Made `handle()` a regular sync `fn`. The trait is deliberately synchronous: ```rust pub trait HookHandler: Send + Sync { fn handle(&self, event: HookEvent) -> Result, PiklError>; } ``` This works because handlers don't actually need to `await` anything at the call site: - **Exec hooks** spawn a subprocess via `tokio::spawn` and return immediately. The process runs in the background. - **Handler hooks** write to an internal channel via `try_send` (non-blocking) and return. A background task handles the actual I/O. Both return `Ok(vec![])` instantly. Responses from handler hooks flow back through the action channel asynchronously. ## Takeaway `async fn in trait` (edition 2024) is for generic/static dispatch. If you need `dyn Trait`, you still need either: 1. A sync method (if the work can be spawned internally) 2. A boxed future return type 3. The `async-trait` crate Option 1 is the simplest when the handler doesn't need to return data from async work. If responses are needed, they go through a separate channel rather than the return value. ## Related: tokio test-util for debounce tests Tests that verify debounce timing need `tokio::time::pause` and `tokio::time::advance`, which require the `test-util` feature on tokio in dev-dependencies. Use `#[tokio::test(start_paused = true)]` and `tokio::time::sleep()` (which auto-advances in paused mode) rather than manual `pause()` + `advance()` + `yield_now()` chains, which are fragile with spawned tasks.