.claude/skills/tauri-2/tauri-errors/tauri-errors-ipc/SKILL.md
Use when encountering invoke errors, serialization failures, or IPC-related panics in Tauri 2. Prevents silent type mismatches between Rust and JavaScript and missing Serialize on error types. Covers serialization failures, command not found, argument type mismatches, permission denied, async panics, and thiserror patterns. Keywords: tauri IPC error, invoke error, serialization failure, command not found, type mismatch, thiserror, Serialize.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer tauri-errors-ipcInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
invoke() fails or returns unexpected result
|
+-- Error: "command <name> not found"
| --> Step 1: Command Registration (Section 1)
|
+-- Error: "command <name> not allowed"
| --> Permission issue. See tauri-errors-permissions skill.
|
+-- Error contains "invalid type" / "missing field" / "invalid value"
| --> Step 2: Serialization & Type Mismatch (Section 2)
|
+-- Error: invoke returns string instead of object (or vice versa)
| --> Step 3: Error Serialization Pattern (Section 3)
|
+-- Rust panics (app crashes, no error returned to JS)
| --> Step 4: Async Command Panics (Section 4)
|
+-- Error caught but message is unhelpful ("null" or empty string)
| --> Step 5: Structured Error Pattern (Section 5)
|
+-- No error, but invoke never resolves (hangs forever)
| --> Step 6: Deadlocks & Blocking (Section 6)
command <name> not foundinvoke('my_command') rejects immediatelyStep 1.1: Verify the command is registered in generate_handler![]:
// src-tauri/src/lib.rs
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
my_command, // Direct function
commands::my_command, // Module-prefixed function
])
Step 1.2: Verify there is only ONE invoke_handler() call. Multiple calls do NOT merge -- only the last one takes effect:
// WRONG: first handler is silently overwritten
builder
.invoke_handler(tauri::generate_handler![cmd_a])
.invoke_handler(tauri::generate_handler![cmd_b]) // Only this one works
Step 1.3: Verify the function has the #[tauri::command] attribute:
#[tauri::command] // REQUIRED -- without this, generate_handler! will fail to compile
fn my_command() -> String {
"hello".into()
}
Step 1.4: Verify the command name matches. Rust uses snake_case function names. The frontend calls the same snake_case name:
// Command: fn get_user_data() -> ...
await invoke('get_user_data'); // CORRECT: snake_case
await invoke('getUserData'); // WRONG: command names are NOT auto-converted
Step 1.5: If the command is in a separate module, verify it is pub:
// src-tauri/src/commands.rs
pub fn my_command() { } // MUST be pub when in a module
// src-tauri/src/lib.rs
fn my_command() { } // MUST NOT be pub when in lib.rs directly
invalid type: expected <X>, found <Y>missing field '<name>'unknown field '<name>'null or undefined unexpectedlyStep 2.1: Check argument name casing. Rust parameters use snake_case. JavaScript arguments MUST use camelCase:
#[tauri::command]
fn save_file(file_path: String, file_size: u64) { }
// CORRECT: camelCase keys matching snake_case params
await invoke('save_file', { filePath: '/doc.txt', fileSize: 1024 });
// WRONG: snake_case keys -- causes "missing field" error
await invoke('save_file', { file_path: '/doc.txt', file_size: 1024 });
Step 2.2: Check type compatibility. Common mismatches:
| Rust Type | JavaScript Type | Common Mistake |
|-----------|----------------|----------------|
| String | string | Passing number or null |
| u32 / i32 | number | Passing string like "42" |
| f64 | number | Passing NaN or Infinity (fails serde) |
| bool | boolean | Passing 0/1 instead of true/false |
| Vec<T> | T[] | Passing non-array iterable |
| Option<T> | T \| null | Passing undefined (use null explicitly) |
| PathBuf | string | Passing object instead of path string |
Step 2.3: Check that custom struct derives Deserialize (for arguments) and Serialize (for return types):
use serde::{Deserialize, Serialize};
#[derive(Deserialize)] // REQUIRED for command arguments
struct CreateRequest {
name: String,
count: u32,
}
#[derive(Serialize)] // REQUIRED for return types
struct CreateResponse {
id: u64,
created: bool,
}
#[tauri::command]
fn create(request: CreateRequest) -> CreateResponse { ... }
Step 2.4: Verify rename_all attribute if used:
// This changes the expected JS argument convention
#[tauri::command(rename_all = "snake_case")]
fn my_cmd(user_name: String) { }
// Now JS must use: invoke('my_cmd', { user_name: 'Alice' })
Result<T, E> but frontend receives unhelpful error"null" or "" or a raw Rust debug stringthe trait Serialize is not implemented for <ErrorType>The error type in Result<T, E> MUST implement Serialize. The idiomatic approach uses thiserror with a manual Serialize impl:
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("database error: {0}")]
Database(String),
#[error("not found")]
NotFound,
}
// REQUIRED: Manual Serialize impl -- serializes as the Display 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]
fn open_file(path: String) -> Result<String, Error> {
let content = std::fs::read_to_string(path)?; // auto-converts via #[from]
Ok(content)
}
NEVER use Result<T, String> for anything beyond prototyping. It loses error type information and makes frontend error handling fragile.
NEVER derive Serialize on error enums that contain non-serializable types (like std::io::Error). Use the manual Serialize impl pattern above.
ALWAYS implement Display (via thiserror) AND Serialize on error types. Both are required for IPC.
thread 'tokio-runtime-worker' panickedinvoke() never resolves (hangs in the frontend)Step 4.1: Check for .unwrap() or .expect() on fallible operations inside async commands. Replace with ? operator:
// WRONG: panics on error, crashes the tokio worker thread
#[tauri::command]
async fn read_data(path: String) -> String {
std::fs::read_to_string(path).unwrap() // PANICS on missing file
}
// CORRECT: returns error to frontend
#[tauri::command]
async fn read_data(path: String) -> Result<String, Error> {
let content = std::fs::read_to_string(path)?;
Ok(content)
}
Step 4.2: Check for borrowed references in async commands. Async commands cannot use &str directly:
// WRONG: compilation error or runtime panic
#[tauri::command]
async fn process(value: &str) -> String {
value.to_uppercase()
}
// CORRECT: use owned types
#[tauri::command]
async fn process(value: String) -> String {
value.to_uppercase()
}
// ALSO CORRECT: wrap return in Result to allow &str
#[tauri::command]
async fn process_ref(value: &str) -> Result<String, ()> {
Ok(value.to_uppercase())
}
Step 4.3: Check for blocking calls inside async commands. Use tokio::task::spawn_blocking for CPU-heavy or blocking I/O work:
#[tauri::command]
async fn heavy_compute(data: Vec<u8>) -> Result<Vec<u8>, Error> {
let result = tokio::task::spawn_blocking(move || {
// CPU-intensive work here
process_data(&data)
}).await.map_err(|e| Error::Internal(e.to_string()))?;
Ok(result)
}
For errors the frontend needs to handle differently by type, use a tagged serde enum:
#[derive(serde::Serialize)]
#[serde(tag = "kind", content = "message")]
#[serde(rename_all = "camelCase")]
enum AppError {
Io(String),
NotFound(String),
Unauthorized(String),
Validation(String),
}
#[tauri::command]
fn load_data(id: u64) -> Result<Data, AppError> {
// ...
}
Frontend receives typed error objects:
try {
await invoke('load_data', { id: 42 });
} catch (err: unknown) {
const error = err as { kind: string; message: string };
switch (error.kind) {
case 'notFound':
showNotFoundUI(error.message);
break;
case 'unauthorized':
redirectToLogin();
break;
case 'validation':
showValidationError(error.message);
break;
default:
showGenericError(error.message);
}
}
invoke() hangs forever, never resolves or rejectsStep 6.1: Check for synchronous commands doing I/O. Sync commands run on the main thread and block the entire UI:
// WRONG: blocks main thread
#[tauri::command]
fn read_file(path: String) -> String {
std::fs::read_to_string(path).unwrap() // Blocks main thread
}
// CORRECT: async command runs on tokio runtime
#[tauri::command]
async fn read_file(path: String) -> Result<String, Error> {
let content = tokio::fs::read_to_string(path).await?;
Ok(content)
}
Step 6.2: Check for nested Mutex locks (deadlock):
// WRONG: if any code path locks the same mutex twice, deadlock
#[tauri::command]
fn update(state: tauri::State<'_, Mutex<AppData>>) {
let mut data = state.lock().unwrap();
// ... calls a function that also locks state ...
helper(&state); // DEADLOCK if helper also calls state.lock()
}
Step 6.3: Check for std::sync::Mutex held across .await:
// WRONG: std::sync::Mutex blocks the async runtime
#[tauri::command]
async fn bad(state: tauri::State<'_, std::sync::Mutex<Data>>) -> Result<(), Error> {
let data = state.lock().unwrap();
some_async_call().await; // Holding std Mutex across await = potential deadlock
Ok(())
}
// CORRECT: use tokio::sync::Mutex for locks held across await points
#[tauri::command]
async fn good(state: tauri::State<'_, tokio::sync::Mutex<Data>>) -> Result<(), Error> {
let data = state.lock().await;
some_async_call().await;
Ok(())
}
ALWAYS wrap invoke() calls in try/catch. Unhandled rejections cause silent failures:
import { invoke } from '@tauri-apps/api/core';
// Minimal pattern
try {
const result = await invoke<string>('my_command', { arg: 'value' });
// handle success
} catch (error: unknown) {
console.error('Command failed:', error);
// handle error
}
// Typed wrapper pattern (recommended for reuse)
async function safeInvoke<T>(
cmd: string,
args?: Record<string, unknown>
): Promise<{ data: T; error: null } | { data: null; error: string }> {
try {
const data = await invoke<T>(cmd, args);
return { data, error: null };
} catch (err) {
return { data: null, error: String(err) };
}
}
development
Use when integrating Vite with a backend framework, rendering Vite assets from server-side templates, or setting up dev/production HTML serving. Prevents incorrect manifest.json traversal and missing CSS chunk resolution in production. Covers build.manifest configuration, .vite/manifest.json structure, ManifestChunk properties, dev mode HTML setup, production rendering, CSS/JS chunk resolution, and modulepreload polyfill. Keywords: backend integration, manifest.json, ManifestChunk, Django, Laravel, Rails, modulepreload.
development
Use when encountering dev server startup failures, HMR issues, proxy errors, CORS blocks, or module not found errors during development. Prevents misconfiguring server.hmr behind reverse proxies and forgetting appType: 'custom' in middleware mode. Covers HMR full-reload debugging, proxy configuration, CORS setup, HTTPS certificates, server.fs.strict violations, port conflicts, WebSocket failures, file watcher issues, and middleware mode. Keywords: dev server, HMR, proxy, CORS, HTTPS, WebSocket, port conflict, server.fs.strict, middleware mode, file watcher.
development
Use when encountering pre-bundling errors, dependency resolution failures, stale cache issues, or slow development server startup. Prevents excluding CJS dependencies from pre-bundling (which breaks runtime module resolution) and misconfiguring optimizeDeps. Covers CJS/ESM conversion failures, missing dependency auto-discovery, optimizeDeps configuration, monorepo linked dependencies, cache invalidation, browser cache staleness, and large dependency tree performance. Keywords: pre-bundling, optimizeDeps, CJS, ESM, cache, dependency resolution, monorepo, node_modules/.vite.
development
Use when encountering Vite build failures, chunk size warnings, or version-specific build errors. Prevents the common mistake of using deprecated rollupOptions in v8 or misconfiguring build targets and minifiers. Covers Rolldown/Rollup bundling failures, CSS minification errors, sourcemap problems, library mode build failures, BundleError handling, and asset processing errors. Keywords: build error, Rolldown, chunk size, sourcemap, library mode, minify, BundleError, rollupOptions, build.target.