tauri-errors-runtime

Installation
SKILL.md

tauri-errors-runtime

Quick Diagnosis

When a Tauri 2 app crashes or behaves unexpectedly at runtime, identify the error category:

Runtime Error Categories:
1. State panics       --> Type mismatch, unmanaged state
2. IPC errors         --> Command not found, permission denied, argument mismatch
3. Window errors      --> Window/webview not found, already destroyed
4. Plugin errors      --> Plugin not initialized, missing permissions
5. Event errors       --> Invalid event name, listener leaks
6. Asset errors       --> File not found, protocol not configured
7. Panic/crash        --> Unhandled panic, thread panic

ALWAYS check the terminal/console output -- Tauri prints panics and errors to stderr.

NEVER ignore unwrap() calls in production code -- they cause panics that crash the app.


Error Lookup Table: State Management

ERR-R001: State type mismatch panic

Error: state not managed for field "state" on command "my_command" or called Option::unwrap() on a None value at runtime.

Cause: The type in State<'_, T> does not match the type passed to manage(). This is a RUNTIME panic, not a compile error.

// WRONG -- register Mutex<Counter>, access as Counter
app.manage(Mutex::new(Counter::default()));

#[tauri::command]
fn bad(state: State<'_, Counter>) { }  // PANIC at runtime!

// CORRECT -- types must match exactly
app.manage(Mutex::new(Counter::default()));

#[tauri::command]
fn good(state: State<'_, Mutex<Counter>>) {
    let mut counter = state.lock().unwrap();
    counter.value += 1;
}

ALWAYS ensure the generic type in State<'_, T> matches exactly what was passed to manage().

ERR-R002: State not registered

Error: state not managed for field "state" -- app panics when the command is first invoked.

Cause: Forgot to call .manage() on the Builder or in setup().

Fix: ALWAYS register state before it is accessed:

tauri::Builder::default()
    .manage(AppConfig { api_url: "https://api.example.com".into() })
    .manage(Mutex::new(Counter::default()))
    // ... state must be registered before commands use it

ERR-R003: Deadlock from nested Mutex locks

Error: App freezes (hangs) -- no panic, no error, just unresponsive.

Cause: Locking the same Mutex twice in the same call chain.

Fix: NEVER lock the same Mutex more than once in a scope. Use RwLock for read-heavy patterns:

// WRONG -- deadlock
#[tauri::command]
fn bad(state: State<'_, Mutex<Data>>) {
    let data = state.lock().unwrap();
    helper(&state);  // Tries to lock again -> DEADLOCK
}

fn helper(state: &Mutex<Data>) {
    let data = state.lock().unwrap();  // Hangs forever
}

// CORRECT -- lock once, pass the guard
#[tauri::command]
fn good(state: State<'_, Mutex<Data>>) {
    let mut data = state.lock().unwrap();
    helper(&mut data);
}

fn helper(data: &mut Data) {
    // Works with the already-locked data
}

ERR-R004: Wrapping state in unnecessary Arc

Error: No error, but redundant allocation. Can cause confusion when accessing state.

Cause: Tauri already wraps managed state in Arc internally.

// WRONG -- double Arc
app.manage(Arc::new(MyState::default()));

// CORRECT -- Tauri handles Arc internally
app.manage(MyState::default());

Error Lookup Table: IPC / Commands

ERR-R010: Command not found at runtime

Error: Frontend receives command my_command not found or promise rejects with "command not allowed."

Causes (check in order):

  1. Command not registered in generate_handler![]
  2. Permission not granted in capabilities file
  3. Command name mismatch (Rust uses snake_case, JS passes the same name)

Fix checklist:

[ ] Command listed in generate_handler![my_command]
[ ] Permission defined in src-tauri/permissions/*.toml
[ ] Permission referenced in src-tauri/capabilities/default.json
[ ] Command name matches exactly (including module path for plugin commands)

ERR-R011: Argument type mismatch

Error: invalid type: expected X, found Y or missing field "fieldName" at runtime.

Cause: Frontend passes wrong types or wrong key names to invoke().

Rules:

  • Frontend keys MUST be camelCase (Tauri auto-converts to Rust snake_case)
  • Types must match: JS number -> Rust i32/u64/f64, JS string -> Rust String, etc.
// WRONG -- snake_case keys
await invoke('save_file', { file_path: '/doc.txt' });

// CORRECT -- camelCase keys
await invoke('save_file', { filePath: '/doc.txt' });

ERR-R012: Unhandled invoke rejection

Error: Unhandled Promise Rejection in the browser console.

Cause: Rust command returns Err(...) but frontend does not catch the error.

// WRONG
const data = await invoke('risky_operation');

// CORRECT
try {
    const data = await invoke('risky_operation');
} catch (error) {
    console.error('Operation failed:', error);
}

ALWAYS wrap invoke() calls in try/catch when the Rust command returns Result.


Error Lookup Table: Window / Webview

ERR-R020: Window not found

Error: get_webview_window("label") returns None, causing unwrap panic or silent failure.

Cause: Window label does not match, window was destroyed, or window was not created yet.

Fix: ALWAYS use Option handling:

// WRONG
let window = app.get_webview_window("settings").unwrap();  // PANIC if not found

// CORRECT
if let Some(window) = app.get_webview_window("settings") {
    window.show().unwrap();
    window.set_focus().unwrap();
} else {
    // Window does not exist -- create it or log the issue
}

ERR-R021: Window operations after destruction

Error: Various errors when calling methods on a destroyed window.

Fix: ALWAYS check if the window still exists before operating on it. Use try_* methods where available.


Error Lookup Table: Plugin Errors

ERR-R030: Plugin not initialized

Error: plugin X not initialized or plugin command not found at runtime.

Cause: Plugin not registered in the Builder chain.

Fix: ALWAYS register plugins before using their commands:

tauri::Builder::default()
    .plugin(tauri_plugin_fs::init())
    .plugin(tauri_plugin_dialog::init())
    .plugin(tauri_plugin_shell::init())
    .plugin(tauri_plugin_opener::init())
    // ...

ERR-R031: Plugin permissions not configured

Error: Unhandled Promise Rejection: command plugin:fs|read not allowed or similar.

Cause: Plugin is registered in Rust but permissions are not granted in capabilities.

Fix: Add permissions to capability file:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:default",
    "fs:allow-read-file",
    "dialog:default",
    "shell:default"
  ]
}

ALWAYS add both the plugin Cargo dependency AND the capability permissions.


Error Lookup Table: Events

ERR-R040: Event name validation panic

Error: Panic with message about invalid event name characters.

Cause: Event names may ONLY contain: alphanumeric characters, -, /, :, and _. Spaces, dots, and special characters cause panics.

// WRONG -- dot in event name
app_handle.emit("data.updated", payload)?;  // PANIC

// WRONG -- space in event name
app_handle.emit("data updated", payload)?;  // PANIC

// CORRECT
app_handle.emit("data-updated", payload)?;
app_handle.emit("data:updated", payload)?;
app_handle.emit("data_updated", payload)?;
app_handle.emit("data/updated", payload)?;

NEVER use characters other than [a-zA-Z0-9-/:_] in event names.

ERR-R041: Event listener memory leak

Error: Increasing memory usage, duplicate event handling, React component "ghost" listeners.

Cause: Event listeners not cleaned up when components unmount.

// WRONG -- listener leaks on unmount
useEffect(() => {
    listen('update', handler);
}, []);

// CORRECT
useEffect(() => {
    const promise = listen('update', handler);
    return () => { promise.then(fn => fn()); };
}, []);

ALWAYS return the unlisten function in cleanup handlers.

ERR-R042: Forgetting await on listen

Error: TypeError: unlisten is not a function when trying to unsubscribe.

Cause: listen() returns Promise<UnlistenFn>, not UnlistenFn directly.

// WRONG
const unlisten = listen('event', handler);
unlisten();  // TypeError!

// CORRECT
const unlisten = await listen('event', handler);
unlisten();

Error Lookup Table: Assets & Protocols

ERR-R050: Asset protocol not working

Error: Images/files not loading, asset:// URLs return 404.

Cause: Asset protocol not enabled or scope not configured.

Fix: Enable and scope in tauri.conf.json:

{
  "app": {
    "security": {
      "assetProtocol": {
        "enable": true,
        "scope": ["$APPDATA/**", "$RESOURCE/**"]
      }
    }
  }
}

ERR-R051: CSP blocking resources

Error: Refused to load the script/image/style in browser console.

Cause: Content Security Policy too restrictive.

Fix: Update CSP in tauri.conf.json:

{
  "app": {
    "security": {
      "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost data:; connect-src ipc: http://ipc.localhost"
    }
  }
}

Error Lookup Table: Panics & Crashes

ERR-R060: Unhandled panic crashes the app

Error: App exits abruptly with panic message in console.

Cause: unwrap() or expect() on None/Err values.

Fix: ALWAYS handle errors with Result and Option:

// WRONG -- panics on error
let content = std::fs::read_to_string(path).unwrap();

// CORRECT -- return Result
#[tauri::command]
fn read_file(path: String) -> Result<String, AppError> {
    let content = std::fs::read_to_string(path)?;
    Ok(content)
}

ERR-R061: Main thread blocked

Error: App window freezes, becomes unresponsive, or shows "Not Responding."

Cause: Synchronous command doing I/O or heavy computation on the main thread.

Fix: ALWAYS use async for I/O operations:

// WRONG -- blocks main thread
#[tauri::command]
fn read_big_file(path: String) -> String {
    std::fs::read_to_string(path).unwrap()  // Freezes UI
}

// CORRECT -- async, runs on tokio runtime
#[tauri::command]
async fn read_big_file(path: String) -> Result<String, String> {
    tokio::fs::read_to_string(path).await.map_err(|e| e.to_string())
}

Diagnostic Decision Tree

App crashes or misbehaves at runtime?
|
+-- Is it a panic (app exits)?
|   +-- "state not managed" --> ERR-R001, ERR-R002
|   +-- "unwrap on None" --> ERR-R020, ERR-R060
|   +-- Event name invalid --> ERR-R040
|
+-- Is the UI frozen?
|   +-- Sync command doing I/O --> ERR-R061
|   +-- Deadlock --> ERR-R003
|
+-- Is a command failing?
|   +-- "command not found/allowed" --> ERR-R010, ERR-R031
|   +-- "invalid type" or argument error --> ERR-R011
|   +-- "Unhandled Promise Rejection" --> ERR-R012
|
+-- Are assets not loading?
|   +-- asset:// 404 --> ERR-R050
|   +-- CSP blocking --> ERR-R051
|
+-- Memory growing / duplicate events?
    +-- Listener leak --> ERR-R041
    +-- Await missing --> ERR-R042

Panic Prevention Checklist

  1. NEVER use .unwrap() in command handlers -- use ? with Result
  2. NEVER use &str in async command parameters -- use String
  3. ALWAYS match State<'_, T> exactly with the type passed to manage()
  4. ALWAYS guard get_webview_window() with if let Some
  5. ALWAYS use only [a-zA-Z0-9-/:_] in event names
  6. ALWAYS clean up event listeners on component unmount
  7. ALWAYS use async commands for I/O, network, or heavy computation
  8. ALWAYS wrap invoke() in try/catch on the frontend

Reference Links

Official Sources

Related skills
Installs
1
GitHub Stars
1
First Seen
Apr 2, 2026