Choosing Between Tauri Invoke Commands and Custom Events for Frontend‑Backend Communication
Guide to choosing Tauri invoke commands vs custom events, with trade‑offs and a validation example.
15 Sept 2026, 02:54 UTC

Decision and Constraints
When building a Tauri application you must decide how the frontend (HTML/JS) talks to the Rust backend. The decision hinges on latency, type safety, boilerplate, and compatibility with Tauri’s CSP. The stable release ≥1.2.0 supports two primary mechanisms.
Supported Options
| Option | Sync/Async | Type Safety | Boilerplate | Typical Use |
|---|---|---|---|---|
Invoke ([tauri::command]) | Sync (can be async fn) | High (compile‑checked args/return) | Low (macro) | RPC‑style calls, data fetch |
Custom events (Window::emit / listen) | Async (fire‑and‑forget) | None (string payload) | Moderate (event names) | Notifications, streams, UI updates |
Trade‑offs
Invoke gives compile‑time guarantees and automatic JSON serialization, but the frontend is coupled to the exact Rust signature and a heavyweight command blocks the Tauri event loop, freezing the UI.
Custom events decouple the sides, allow parallel handling, and avoid blocking the main loop, yet they require manual serialization, offer no type safety, and error propagation must be handled explicitly.
Concrete Implementation (Invoke)
Define a command in src-tauri/src/main.rs:
#[tauri::command]
fn greet(name: String) -> Result {
Ok(format("Hello, {}!", name))
}
Register it with the Tauri builder:
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Call it from a Svelte (or React) frontend:
async function sayHello() {
const reply = await window.__TAURI__.invoke('greet', { name: 'Tauri' });
console.log(reply); // "Hello, Tauri!"
}
To validate, run cargo tauri dev, open the developer console, execute sayHello() and confirm the promise resolves with the expected string. A console.log placed after the invoke call will appear only after the promise settles, demonstrating the request‑reply behavior.
Concrete Implementation (Custom Events)
Emit an event from Rust:
fn main() {
tauri::Builder::default()
.setup(|app| {
let window = app.get_window("main").unwrap();
window.emit("notification", Some("Welcome!")).unwrap();
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Listen in the frontend:
window.__TAURI__.event.listen('notification', (event) => {
console.log(event.payload); // "Welcome!"
});
Verification: after starting dev mode, check that the listener fires exactly once and receives the correctly deserialized payload. Ensure the payload is JSON‑serializable; otherwise the event will fail silently.
Limitations and Practical Checks
- Invoke: avoid long‑running work; off‑load to
std::threadortokioif needed. - Events: version‑check payloads or use a schema library to avoid runtime mismatches.
Practical check: for invoke, measure round‑trip time with performance.now() before and after the call; for events, record timestamps in the emitter and listener to confirm asynchronous delivery.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.