Step 3
IPC — command / event
25 min
IPC — command / event
Two ways the frontend (TypeScript) and backend (Rust) talk.
- command (invoke) — frontend → Rust, returns a value. Like a function call.
- event (emit / listen) — two-way async notifications. Pub/sub.
1. Rust command
#[tauri::command]
fn greet(name: &str) -> String { format!("Hello, {}!", name) }
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
tokio::fs::read_to_string(&path).await.map_err(|e| e.to_string())
}
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, read_file])
.run(tauri::generate_context!()).expect("error");
}
2. Frontend invoke
import { invoke } from "@tauri-apps/api/core";
const msg = await invoke<string>("greet", { name: "World" });
try {
const content = await invoke<string>("read_file", { path: "/etc/hosts" });
} catch (e) { console.error(e); }
Rust snake_case names map to JS camelCase parameters.
3. Events
use tauri::Emitter;
#[tauri::command]
async fn start_job(app: tauri::AppHandle) -> Result<(), String> {
for i in 1..=100 {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
app.emit("job-progress", i).map_err(|e| e.to_string())?;
}
app.emit("job-done", ()).map_err(|e| e.to_string())?;
Ok(())
}
import { listen } from "@tauri-apps/api/event";
const unlisten = await listen<number>("job-progress", (e) => {
console.log("progress:", e.payload);
});
await invoke("start_job");
unlisten();
Clean up listeners or they run after unmount.
4. Capabilities
src-tauri/capabilities/default.json:
{
"identifier": "default",
"windows": ["main"],
"permissions": ["core:default", "dialog:allow-open", "fs:allow-read-text-file"]
}
Deny by default, opt into what you actually need.
5. Typed errors
#[derive(Debug, thiserror::Error, serde::Serialize)]
enum Error {
#[error("io error: {0}")] Io(String),
#[error("not found")] NotFound,
}
Catch narrowly on the frontend.
6. Gotchas
- Missing
generate_handler!registration - Blocking sync Rust for I/O — use
tokio::fs - Duplicate listeners on re-render — cleanup
- Non-serializable Rust types — add
#[derive(serde::Serialize)]
Closing
Think of commands as HTTP GET/POST and events as WebSocket/SSE. ~90% of needs fit into commands.
Next
- 04-local-sqlite