.claude/skills/tauri-2/tauri-errors/tauri-errors-runtime/SKILL.md
Use when encountering runtime panics, state errors, or unexpected app crashes in Tauri 2. Prevents unhandled unwrap() panics in production and unmanaged state type mismatches that crash the app. Covers window not found, state not managed panics, plugin not initialized, asset resolution, event name validation, and panic handling. Keywords: tauri runtime error, panic, state not managed, window not found, plugin not initialized, unwrap, crash.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer tauri-errors-runtimeInstall 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.
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: 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().
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
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
}
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: Frontend receives command my_command not found or promise rejects with "command not allowed."
Causes (check in order):
generate_handler![]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)
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:
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' });
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: 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
}
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: 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())
// ...
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: 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.
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.
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: 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/**"]
}
}
}
}
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: 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)
}
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())
}
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
.unwrap() in command handlers -- use ? with Result&str in async command parameters -- use StringState<'_, T> exactly with the type passed to manage()get_webview_window() with if let Some[a-zA-Z0-9-/:_] in event namesasync commands for I/O, network, or heavy computationinvoke() in try/catch on the frontenddevelopment
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.