Using Tauri’s invoke IPC to Call Rust from Frontend Safely
Learn how Tauri’s invoke bridge lets frontend JavaScript call secure Rust functions with type-safe JSON RPC, see a worked example, and verify behavior.
31 Aug 2025, 15:12 UTC

Problem: UI needs to call heavy Rust logic without exposing unsafe APIs
When building a desktop app with Tauri, you often want to keep the UI in HTML/JavaScript while moving performance‑sensitive or privileged work to Rust. Directly exposing Rust functions through window.__TAURI__ can be error‑prone if you forget to serialize data or handle errors.
Thesis: Tauri’s invoke bridge gives you a type‑safe, JSON‑based RPC that works out of the box, with a small latency cost and a clear threading model.
How the invoke bridge works
Tauri generates a message handler via the tauri::invoke_handler! macro (or its async variant). The handler registers each Rust function under a string name. On the web view side, the global __TAURI__ object (or the @tauri-apps/api package) provides an invoke function that:
- serialises the payload with serde_json,
- sends it over the internal IPC channel,
- invokes the matching Rust function on the main thread unless the handler is marked async,
- deserialises the return value and resolves the promise.
Worked example: a greeting function
- Add the dependency in src-tauri/Cargo.toml:
- Define a Rust function in src-tauri/src/main.rs:
- In the frontend (e.g., src/index.html with a script tag or a framework), call:
- Run the dev server:
- Run cargo tauri dev and open the developer console.
- Execute await window.__TAURI__.invoke("greet", { name: "Test" }) and confirm the returned string matches the Rust function’s output.
- Replace the handler with a blocking version (std::thread::sleep(std::time::Duration::from_secs(2))) and notice the UI becomes unresponsive for two seconds, confirming the default main‑thread execution.
- Switch to the async handler (tauri::generate_async_handler![greet_async]) and repeat the call; the UI should stay responsive, demonstrating the threading trade‑off.
[dependencies]
tauri = { version = "2.0", features = ["api-all"] }
use tauri::{generate_handler, InvokeContext};
#[tauri::command]
fn greet(name: String) -> Result {
Ok(format("Hello, {}!", name))
}
fn main() {
tauri::Builder::default()
.invoke_handler(generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
async function sayHello() {
try {
const reply = await window.__TAURI__.invoke("greet", { name: "Tauri" });
console.log(reply); // → "Hello, Tauri!"
} catch (e) {
console.error(e);
}
}
sayHello();
# From the project root
cargo tauri dev
Open the browser console; you should see the greeting printed. No additional configuration is required.
Trade‑offs and verification
Latency: Each invoke call serialises arguments, crosses the IPC boundary, and deserialises the result. In practice this adds roughly 0.5-2 ms per call on a modern laptop—acceptable for UI‑level interactions but noticeable if you invoke dozens of times per frame.
Threading: By default the handler runs on the main thread. A blocking Rust call (e.g., std::thread::sleep) will freeze the UI. To avoid this, mark the command as async or spawn a task inside the handler:
#[tauri::command]
async fn heavy_work() -> Result<(), String> {
tauri::async_runtime::spawn(async move {
// perform blocking work here
}).await?;
Ok(())
}
Serialization limits: Only types that implement serde::Serialize and serde::Deserialize can cross the bridge. Complex handles like std::fs::File must be wrapped or replaced with paths.
Practical verification steps
Actionable closing
If you need to call Rust from your Tauri frontend, start with the synchronous #[tauri::command] and invoke pattern. Measure the latency in your specific UI workflow; if it becomes a bottleneck, move the heavy work onto an async task or a separate thread. Always verify that your argument and return types are serialisable, and use the developer console to catch serialization errors early.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.