| name | tauri-syntax-commands |
| description | Use when writing Tauri commands, calling invoke(), handling IPC errors, or streaming data from Rust to JavaScript. Prevents command registration omissions, argument type mismatches between Rust and JS, and missing error serialization. Covers #[tauri::command] macro, sync/async commands, argument types, return types, thiserror, invoke() API, and Channel streaming. Keywords: tauri command, invoke, IPC, async command, thiserror, Channel, generate_handler, command registration.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Tauri 2.x with Rust and TypeScript. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
tauri-syntax-commands
Quick Reference
Command Definition (Rust)
| Pattern | Syntax | Notes |
|---|
| Sync command | #[tauri::command] fn name() -> T | Runs on main thread |
| Async command | #[tauri::command] async fn name() -> T | Spawned on tokio runtime |
| Forced async | #[tauri::command(async)] fn name() -> T | Sync function on tokio runtime |
| Renamed args | #[tauri::command(rename_all = "snake_case")] | JS passes snake_case keys |
Argument Types (Rust <- JS)
| Rust Type | JS Type | Notes |
|---|
i32, u64, f64 | number | Direct mapping |
bool | boolean | Direct mapping |
String | string | Owned string |
Vec<T> | T[] | Array to Vec |
Option<T> | T | null | undefined | Nullable |
PathBuf | string | Path string |
| Custom struct | object | Must #[derive(Deserialize)] |
Return Types (Rust -> JS)
| Rust Return | JS Result | Notes |
|---|
() | void / null | No return value |
T (Serialize) | T | Direct serialization |
Result<T, E> | T or throw E | E must impl Serialize |
Response | ArrayBuffer | Binary data bypass |
Special Injected Parameters (NOT passed from JS)
| Rust Parameter | Purpose | Example |
|---|
app: AppHandle | Application handle | app.emit("event", data) |
window: WebviewWindow | Calling window | window.label() |
state: State<'_, T> | Managed state | state.inner() |
request: tauri::ipc::Request | Raw IPC request | request.headers(), request.body() |
Frontend invoke() API
| Feature | Syntax |
|---|
| Import | import { invoke } from '@tauri-apps/api/core' |
| Basic call | await invoke('command_name') |
| With args | await invoke('cmd', { argName: value }) |
| Typed return | await invoke<ReturnType>('cmd') |
| With headers | await invoke('cmd', args, { headers: {...} }) |
| Binary data | await invoke('cmd', new Uint8Array([...])) |
Critical Warnings
NEVER use snake_case keys in JS invoke() arguments -- Tauri auto-converts camelCase to snake_case. Passing { file_path: "x" } will NOT match Rust parameter file_path.
NEVER forget to implement Serialize on error types -- Result<T, E> requires E to implement Serialize to send errors to the frontend.
NEVER use &str parameters in async commands -- borrowed references cannot cross the async spawn boundary. Use String instead.
NEVER register commands with multiple invoke_handler() calls -- only the last one takes effect.
NEVER mark commands in lib.rs as pub -- glue code generation prevents it. Move to a separate module if pub is needed.
ALWAYS wrap invoke() calls in try/catch -- Rust Result::Err rejects the JS Promise.
ALWAYS use Channel<T> for streaming data instead of multiple events -- channels are tied to the command invocation and type-safe.
ALWAYS derive serde::Deserialize on custom argument types and serde::Serialize on custom return types.
Essential Patterns
Pattern 1: Basic Command (Rust + TypeScript)
#[tauri::command]
pub fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
import { invoke } from '@tauri-apps/api/core';
const greeting = await invoke<string>('greet', { name: 'World' });
console.log(greeting);
Pattern 2: Async Command with Error Handling
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("not found: {0}")]
NotFound(String),
}
impl serde::Serialize for Error {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::ser::Serializer {
serializer.serialize_str(self.to_string().as_ref())
}
}
#[tauri::command]
async fn read_file(path: String) -> Result<String, Error> {
let content = tokio::fs::read_to_string(&path).await?;
Ok(content)
}
try {
const content = await invoke<string>('read_file', { path: '/tmp/data.txt' });
console.log(content);
} catch (error) {
console.error('Failed:', error);
}
Pattern 3: Structured Error Types
For richer error handling on the frontend, use tagged enum serialization.
#[derive(serde::Serialize)]
#[serde(tag = "kind", content = "message")]
#[serde(rename_all = "camelCase")]
enum AppError {
Io(String),
NotFound(String),
Unauthorized(String),
}
#[tauri::command]
async fn load_data(id: u32) -> Result<Data, AppError> {
if id == 0 {
return Err(AppError::NotFound("Item not found".into()));
}
Ok(Data { })
}
try {
const data = await invoke<Data>('load_data', { id: 42 });
} catch (err: any) {
switch (err.kind) {
case 'notFound':
showNotFoundUI(err.message);
break;
case 'unauthorized':
redirectToLogin();
break;
default:
console.error('Unexpected error:', err);
}
}
Pattern 4: Special Injected Parameters
These parameters are injected by Tauri -- the frontend does NOT pass them.
use tauri::{AppHandle, State, WebviewWindow};
#[tauri::command]
async fn complex_command(
app: AppHandle,
window: WebviewWindow,
state: State<'_, MyState>,
query: String,
limit: Option<u32>,
) -> Result<Vec<Item>, String> {
println!("Called from window: {}", window.label());
let config = state.inner();
Ok(vec![])
}
const items = await invoke<Item[]>('complex_command', {
query: 'search term',
limit: 10,
});
Pattern 5: Channel Streaming (Rust -> JS)
use tauri::ipc::Channel;
#[derive(Clone, serde::Serialize)]
struct DownloadProgress {
bytes_read: u64,
total: u64,
percent: f64,
}
#[tauri::command]
async fn download_file(
url: String,
on_progress: Channel<DownloadProgress>,
) -> Result<String, String> {
let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
let total = response.content_length().unwrap_or(0);
let mut bytes_read = 0u64;
on_progress.send(DownloadProgress {
bytes_read,
total,
percent: 0.0,
}).unwrap();
Ok("download complete".into())
}
import { invoke, Channel } from '@tauri-apps/api/core';
interface DownloadProgress {
bytesRead: number;
total: number;
percent: number;
}
const onProgress = new Channel<DownloadProgress>();
onProgress.onmessage = (progress) => {
updateProgressBar(progress.percent);
console.log(`${progress.bytesRead}/${progress.total}`);
};
const result = await invoke<string>('download_file', {
url: 'https://example.com/file.zip',
onProgress,
});
Pattern 6: Binary Data with Request/Response
Bypass JSON serialization for large binary payloads.
use tauri::ipc::{Request, Response};
#[tauri::command]
fn upload(request: Request) -> Result<String, String> {
let tauri::ipc::InvokeBody::Raw(data) = request.body() else {
return Err("Expected binary data".into());
};
let auth = request.headers().get("Authorization");
println!("Received {} bytes", data.len());
Ok(format!("Uploaded {} bytes", data.len()))
}
#[tauri::command]
fn get_image(path: String) -> Response {
let data = std::fs::read(path).unwrap();
Response::new(data)
}
const data = new Uint8Array([1, 2, 3, 4, 5]);
const result = await invoke<string>('upload', data, {
headers: { Authorization: 'Bearer token123' },
});
const imageData = await invoke<ArrayBuffer>('get_image', { path: '/photo.jpg' });
Pattern 7: Command Registration
mod commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
commands::greet,
commands::read_file,
commands::download_file,
commands::upload,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Rule: ALL commands MUST be listed in a single generate_handler![] macro call. Multiple invoke_handler() calls do NOT merge.
Argument Name Mapping
Tauri automatically converts between JS camelCase and Rust snake_case.
| Rust Parameter | JS Argument Key |
|---|
file_path: String | filePath |
user_name: String | userName |
item_count: u32 | itemCount |
is_active: bool | isActive |
To override this behavior:
#[tauri::command(rename_all = "snake_case")]
fn my_command(file_path: String) { }
await invoke('my_command', { file_path: '/tmp/file.txt' });
Channel vs Events
| Feature | Channel | Events |
|---|
| Direction | Rust -> JS (response stream) | Bidirectional |
| Lifetime | Tied to command invocation | App lifetime |
| Targeting | Specific caller | Broadcast or filtered |
| Type safety | Generic Channel<T> | Serialized JSON |
| Use case | Progress, streaming data | App-wide notifications |
ALWAYS use Channel<T> for progress updates and streaming data from a command. NEVER use events for command-specific streaming -- channels are scoped to the invocation.
Runtime Generics (Plugin Authors)
When writing commands for plugins or libraries that need to be runtime-agnostic:
use tauri::{AppHandle, Runtime, WebviewWindow};
#[tauri::command]
async fn plugin_command<R: Runtime>(
app: AppHandle<R>,
window: WebviewWindow<R>,
data: String,
) -> Result<String, String> {
Ok(format!("Processed: {}", data))
}
With the default wry feature, R resolves to Wry. This generic is only necessary for plugin/library code.
Reference Links
Official Sources