Tauri Command Registration: Tauri 1 vs Tauri 2 and Type-Safe IPC
Calling Rust functions from a Tauri frontend requires more than defining a #[tauri::command]; registration differences between Tauri 1 and 2, type serialization rules, and async best practices determine whether the IPC bridge works or silently fails.
04 Sept 2026, 01:43 UTC

When you want a Tauri app to do more than display web content—access the filesystem, query system info, or run custom logic—you need a command bridge between the JavaScript frontend and the Rust backend. The useful answer up front: always define your function with #[tauri::command], ensure it implements serde::Serialize for returns and JSON-compatible types for arguments, and register the command name in tauri.conf.json if you're on Tauri 1.x, or rely on auto-registration with stricter checks in Tauri 2.x. Prefer #[tauri::command(async)] for any I/O-bound work to keep the UI responsive.
How the mechanism works
Tauri exposes a typed IPC (inter-process communication) bridge. In Rust, you mark a function with #[tauri::command]. The Tauri code generator reads these marks and registers the command name in the application configuration. On the JavaScript side, window.invoke('command-name', payload) sends a serialized message to the main thread, which resolves to the function's return value. The serialization layer uses JSON: only types that can be expressed as JSON objects, arrays, strings, numbers, and booleans pass across the boundary. Rust structs must be annotated with #[derive(Serialize)] and their fields must also be serializable; otherwise the runtime throws a deserialization error.
Worked configuration: Tauri 1.x explicit registration
If you are using Tauri 1.x, the command must appear under the commands table in tauri.conf.json. Omitting the name means the invoke call is either ignored or throws, depending on the security policy. Here is a minimal Rust function:
use tauri::command;
use serde::Serialize;
#[derive(Serialize)]
struct Greeting { name: String }
#[command]
async fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
The corresponding tauri.conf.json excerpt for Tauri 1.x:
{
"commands": {
"greet": {
"handler": "greet"
}
}
}
The frontend then calls:
window.invoke('greet', { name: 'ReadMeFeed' }).then(result => {
console.log(result); // "Hello, ReadMeFeed!"
})
Worked configuration: Tauri 2.x auto-registration with strict typing
Tauri 2.x scans Rust functions at build time and auto-registers commands, but it enforces that return types implement serde::Serialize and arguments consist of JSON-compatible types. Dynamic command names are not allowed; the name must match the Rust function signature exactly. The same Rust function above works without editing tauri.conf.json, but if you pass a Rust HashMap or a custom struct with a Vec field containing non-serializable objects, the build fails or the invoke call rejects at runtime.
Limits and common mistakes
- Omitting the command from
tauri.conf.json(pre‑2.0) – If your project targets Tauri 1.x and the command name is absent from thecommandstable, thewindow.invokecall either silently fails or throws an error depending on the security level. Always verify the generated config contains your command name. - Non‑serializable fields across the IPC boundary – Passing a Rust
Stringis fine; passing aVec<u8>or a struct containing astd::net::TcpStreamwill cause a deserialization failure. Add#[derive(Serialize)]and keep arguments to primitives, arrays, or other serde‑compatible types. - Blocking the main thread – A command without the
asyncattribute runs synchronously on the Tauri main thread. If the function performs disk I/O, network requests, or heavy computation, the UI freezes. Prefer#[tauri::command(async)]and perform the work inside the async block, or offload to a separate thread for truly long-running tasks. - Mixing synchronous and asynchronous signatures – Declaring some commands as
asyncand others as sync can lead to inconsistent error handling and unexpected Promise behavior on the JS side. Pick one pattern per codebase and stick with it.
Practical verification
- Create a new Tauri project:
tauri createorcargo tauri init. - Add a Rust function with
#[tauri::command]and#[derive(Serialize)]. - For Tauri 1.x, add the command to
tauri.conf.jsonundercommands. - In the renderer, run
window.invoke('your-command', { payload }). Open the browser dev tools console; you should see the returned value. - Check the generated
tauri.conf.json– the presence of your command name undercommandsconfirms proper registration for the installed Tauri major version.
Version assumptions
This guide assumes Tauri 1.x or 2.x as distributed via the official Tauri CLI. Behavior may differ with custom forks or older pre‑1.0 builds. Always check the tauri.conf.json commands section after a build to verify registration status.
Common pitfalls summary
- Forgetting
#[derive(Serialize)]on return structs. - Using non‑JSON‑compatible types as arguments (e.g.,
Option<String>without proper serde handling). - Assuming a command is registered automatically without checking the config.
- Writing synchronous commands that block the UI.
The key takeaway: Tauri’s command bridge is powerful, but its safety guarantees hinge on explicit registration (1.x), strict type compliance, and async patterns for any work that isn’t instantaneous. Follow the patterns above and verify via the config and console output before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.