codingstairs
NotesEDULifeContact
⌕Search⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

Get in touch

Send without signing in. Add your email if you'd like a reply.

  • Leave a message anonymously →
  • ✉ warragon112@gmail.com
  • KakaoTalk Open Chat ↗

© 2026 codingstairs

  • Notes
  • EDU
  • Search
  • Life
  • Contact
  • Legal
  • RSS
  • GitHub
EDU›Tauri 2 — desktop · mobile in one codebase›Step 3

Step 3

IPC — command / event

0 views

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

← Step 2

Project setup

Step 4 →

Local SQLite