"A slow synchronous command freezes the app. An async one lets the window keep breathing while the work happens."
Just Add async
To make a command asynchronous, write it as async fn. Tauri runs it on its async runtime (Tokio) and the frontend's invoke Promise resolves when the future completes. This matters because a long synchronous command can block, making your UI feel frozen. Network calls, waiting on a child process, anything I/O-bound — make it async and the app stays responsive.
I/O-bound vs CPU-bound
There's a real distinction. I/O-bound work (HTTP, disk, waiting) fits async perfectly — the runtime parks the task while it waits and runs other work. CPU-bound work (resizing a huge image, hashing, heavy parsing) doesn't benefit from async alone; if you grind the CPU inside an async task you can still stall the runtime. For heavy compute, offload to a blocking thread with tokio::task::spawn_blocking (or a plain std::thread) so the async runtime stays free.
The Send Gotcha
One rule the compiler will enforce: data you hold across an await in an async command must be safe to move between threads (Send). A common trap is holding a std::sync::Mutex guard across an await point — it won't compile. The fix is to drop the lock before awaiting, or use an async-aware lock. You'll meet this properly in the state track; for now, know that 'cannot be sent between threads safely' usually means 'you held a lock across an await.'