{"slug":"desktop-backend-tauri","title":"desktop-backend-tauri","summary":"Tauri 2.x Rust command patterns, state management, error handling, events, channels, testing","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-29T15:28:04.029675Z","repo":{"url":"https://github.com/agents-inc/skills","stars":24,"forks":8,"license":"MIT","updatedAt":"2026-09-07T17:50:55Z"},"bodyHtml":"<hr>\n<h2>name: desktop-backend-tauri\ndescription: Tauri 2.x Rust command patterns, state management, error handling, events, channels, testing</h2>\n<h1>Tauri Rust Backend Patterns</h1>\n<blockquote>\n<p><strong>Quick Guide:</strong> Define commands with <code>#[tauri::command]</code>, register in <code>generate_handler![]</code>. Use <code>State&lt;T&gt;</code> for shared state (wrap mutable fields in <code>Mutex</code>). Error types must implement both <code>serde::Serialize</code> and <code>Display</code> -- use <code>thiserror</code> for ergonomic error enums. Async commands run on Tokio -- borrowed args (<code>&amp;str</code>, <code>State&lt;'_, T&gt;</code>) require <code>Result&lt;T, E&gt;</code> return type. Stream data to frontend via <code>Channel&lt;T&gt;</code> (not events) for high throughput. Emit events with <code>app.emit()</code> for fire-and-forget notifications.</p>\n<p><strong>Current version:</strong> Tauri 2.x (stable). Async runtime is Tokio.</p>\n</blockquote>\n<hr>\n<p>&lt;critical_requirements&gt;</p>\n<h2>CRITICAL: Before Using This Skill</h2>\n<blockquote>\n<p><strong>All code must follow project conventions in CLAUDE.md</strong> (kebab-case, named exports, import ordering, <code>import type</code>, named constants)</p>\n</blockquote>\n<p><strong>(You MUST register every command in <code>tauri::generate_handler![]</code> -- unregistered commands compile fine but silently fail at runtime)</strong></p>\n<p><strong>(You MUST implement <code>serde::Serialize</code> on all error types returned from commands -- Tauri serializes errors across the IPC boundary)</strong></p>\n<p><strong>(You MUST wrap mutable managed state in <code>Mutex</code> or <code>RwLock</code> -- commands run concurrently and <code>State&lt;T&gt;</code> requires <code>Send + Sync</code>)</strong></p>\n<p><strong>(You MUST return <code>Result&lt;T, E&gt;</code> from async commands that use borrowed args (<code>&amp;str</code>, <code>State&lt;'_, T&gt;</code>) -- Rust lifetime rules require it)</strong></p>\n<p><strong>(You MUST use <code>Channel&lt;T&gt;</code> for streaming data to frontend -- events are designed for small payloads, not high-throughput streaming)</strong></p>\n<p>&lt;/critical_requirements&gt;</p>\n<hr>\n<p><strong>Auto-detection:</strong> #[tauri<span>command], tauri</span>command, tauri<span>State, AppHandle, app.manage, generate_handler, tauri</span>ipc<span>Channel, Emitter, Listener, thiserror, tauri</span>test, mock_builder, async tauri command, tauri error handling, tauri state management</p>\n<p><strong>When to use:</strong></p>\n<ul>\n<li>Defining Rust command handlers (sync and async) for frontend invocation</li>\n<li>Managing application state across commands with <code>app.manage()</code> and <code>State&lt;T&gt;</code></li>\n<li>Implementing error types that serialize across the IPC boundary</li>\n<li>Emitting events from Rust to frontend (progress, notifications, background updates)</li>\n<li>Streaming data from Rust to frontend via channels</li>\n<li>Testing Rust commands with Tauri's mock runtime</li>\n<li>Organizing commands into modules as the backend grows</li>\n</ul>\n<p><strong>When NOT to use:</strong></p>\n<ul>\n<li>Frontend invoke patterns and TypeScript types (see the framework-level Tauri skill)</li>\n<li>Permission/capability configuration (see the framework-level Tauri skill)</li>\n<li>Plugin installation and configuration (see the framework-level Tauri skill)</li>\n<li>Window management, system tray, menus (see the framework-level Tauri skill)</li>\n<li>Packaging and distribution (see the framework-level Tauri skill)</li>\n<li>General Rust programming not specific to Tauri APIs</li>\n</ul>\n<p><strong>Key patterns covered:</strong></p>\n<ul>\n<li>Sync and async commands with <code>#[tauri::command]</code> (<a href=\"examples/core.md\">examples/core.md</a>)</li>\n<li>Error handling with <code>thiserror</code> + manual <code>Serialize</code> impl (<a href=\"examples/core.md\">examples/core.md</a>)</li>\n<li>Managed state with <code>Mutex</code>/<code>RwLock</code> and <code>State&lt;T&gt;</code> injection (<a href=\"examples/core.md\">examples/core.md</a>)</li>\n<li><code>AppHandle</code> for accessing app resources from commands (<a href=\"examples/core.md\">examples/core.md</a>)</li>\n<li>Channels for streaming data to frontend (<a href=\"examples/core.md\">examples/core.md</a>)</li>\n<li>Emitting events from Rust (<a href=\"examples/events.md\">examples/events.md</a>)</li>\n<li>Listening for frontend events in Rust (<a href=\"examples/events.md\">examples/events.md</a>)</li>\n<li>Testing commands with mock runtime (<a href=\"examples/testing.md\">examples/testing.md</a>)</li>\n<li>Command organization in modules (<a href=\"examples/core.md\">examples/core.md</a>)</li>\n</ul>\n<p><strong>Detailed resources:</strong></p>\n<ul>\n<li><a href=\"examples/core.md\">examples/core.md</a> - Commands, error handling, state, AppHandle, channels, modules</li>\n<li><a href=\"examples/events.md\">examples/events.md</a> - Emitting and listening for events from Rust</li>\n<li><a href=\"examples/testing.md\">examples/testing.md</a> - Mock runtime, testing commands with state</li>\n<li><a href=\"reference.md\">reference.md</a> - Decision frameworks, quick-lookup tables, lifetime rules</li>\n</ul>\n<hr>\n\n<hr>\n\n<hr>\n<p>&lt;decision_framework&gt;</p>\n<h2>Decision Framework</h2>\n<h3>Command Design</h3>\n<pre><code>How should this command be structured?\n|-- Fast, CPU-only, no I/O?\n|   +-- Sync command: #[tauri::command] fn\n|-- Involves file, network, or long computation?\n|   +-- Async command: #[tauri::command] async fn -&gt; Result&lt;T, E&gt;\n|-- Needs shared app state?\n|   +-- Add State&lt;T&gt; parameter, register with .manage()\n|-- Needs app paths, windows, or event emission?\n|   +-- Add AppHandle parameter, import Manager trait\n|-- Needs to stream data back to frontend?\n|   +-- Add Channel&lt;T&gt; parameter\n+-- Needs raw request headers or binary body?\n    +-- Add tauri::ipc::Request parameter\n</code></pre>\n<h3>Communication Method</h3>\n<pre><code>How should Rust communicate with the frontend?\n|-- Request/response (frontend asks, Rust answers)?\n|   +-- Command (invoke from frontend, return value)\n|-- Ordered stream from a specific operation?\n|   +-- Channel&lt;T&gt; parameter in a command\n|-- Fire-and-forget notification (broadcast)?\n|   +-- Event: app.emit() or app.emit_to()\n+-- Need to run JS in the webview?\n    +-- webview.eval() (escape hatch, avoid if possible)\n</code></pre>\n<h3>Error Strategy</h3>\n<pre><code>How should this command handle errors?\n|-- Quick prototype or simple command?\n|   +-- Result&lt;T, String&gt; with .map_err(|e| e.to_string())\n|-- Production command with multiple error sources?\n|   +-- Custom error enum with thiserror + manual Serialize impl\n|-- Truly unrecoverable (corrupt state, invariant violation)?\n|   +-- panic! (but never unwrap() on expected errors)\n</code></pre>\n<h3>State Mutability</h3>\n<pre><code>How should state be wrapped?\n|-- Read-only config set once at startup?\n|   +-- No wrapper needed: app.manage(Config { ... })\n|-- Read-heavy, infrequent writes?\n|   +-- RwLock&lt;T&gt;: multiple concurrent readers, exclusive writer\n|-- Frequent reads and writes, simple fields?\n|   +-- Mutex&lt;T&gt;: exclusive access for both reads and writes\n+-- Need to hold lock across .await points?\n    +-- tokio::sync::Mutex (not std::sync::Mutex)\n</code></pre>\n<p>&lt;/decision_framework&gt;</p>\n<hr>\n<p>&lt;red_flags&gt;</p>\n<h2>RED FLAGS</h2>\n<p><strong>High Priority Issues:</strong></p>\n<ul>\n<li>Using <code>unwrap()</code> in commands instead of returning <code>Result</code> -- panics crash the command handler, frontend gets a generic error with no details</li>\n<li>Forgetting to register commands in <code>generate_handler![]</code> -- compiles fine, silently fails at runtime</li>\n<li>Missing <code>serde::Serialize</code> on error types -- compilation error, but the fix is non-obvious (manual impl, not derive)</li>\n<li>Using <code>std::sync::Mutex</code> and holding the lock across <code>.await</code> -- blocks the Tokio runtime, causes deadlocks. Use <code>tokio::sync::Mutex</code> when you need to hold across await points</li>\n<li>Forgetting <code>.manage(T)</code> registration -- runtime panic when a command tries to access <code>State&lt;T&gt;</code></li>\n<li>Deriving <code>Serialize</code> on error enums -- produces variant-structure JSON (<code>{\"Io\": {...}}</code>) instead of a readable string</li>\n</ul>\n<p><strong>Medium Priority Issues:</strong></p>\n<ul>\n<li>Using events for high-throughput streaming (download progress, log tailing) -- events are JSON-serialized pub-sub, not optimized for throughput. Use <code>Channel&lt;T&gt;</code></li>\n<li>Using sync commands for I/O operations -- blocks the main thread, freezes the webview</li>\n<li>Not importing <code>tauri::Emitter</code> when calling <code>.emit()</code> -- compilation error with confusing message about missing method</li>\n<li>Returning <code>Option&lt;()&gt;</code> from commands -- serializes as <code>null</code> which the frontend may not expect (serde serialization/deserialization asymmetry)</li>\n</ul>\n<p><strong>Gotchas &amp; Edge Cases:</strong></p>\n<ul>\n<li><strong>Async + borrowed args:</strong> <code>async fn cmd(name: &amp;str)</code> without <code>Result</code> return type fails to compile. Either use <code>String</code> or return <code>Result&lt;T, E&gt;</code></li>\n<li><strong>Argument naming:</strong> Frontend passes camelCase (<code>invokeMessage</code>), Rust receives snake_case (<code>invoke_message</code>) by default. Use <code>#[tauri::command(rename_all = \"snake_case\")]</code> to change this</li>\n<li><strong>State injection order:</strong> <code>State&lt;T&gt;</code> parameters are not passed from frontend -- they are injected by Tauri. Mixing up \"frontend args\" and \"injected params\" in the function signature is confusing but works (Tauri filters them)</li>\n<li><strong>Mutex poisoning:</strong> <code>lock().unwrap()</code> panics if a previous holder panicked. In production, handle <code>PoisonError</code> or use <code>lock().expect(\"state lock poisoned\")</code></li>\n<li><strong>Multiple state types:</strong> Each <code>.manage(T)</code> call registers a separate type. <code>State&lt;Mutex&lt;AppState&gt;&gt;</code> and <code>State&lt;AppState&gt;</code> are different registrations</li>\n<li><strong>Channel lifetime:</strong> <code>Channel&lt;T&gt;</code> is tied to the command invocation. It cannot be stored for later use outside the command</li>\n<li><strong>Event payload types:</strong> Event payloads must be <code>Serialize + Clone</code>. <code>serde_json::Value</code> works as a catch-all but loses type safety</li>\n<li><strong><code>emit_to</code> target:</strong> Target is a webview label string. If the webview does not exist, the event is silently dropped</li>\n</ul>\n<p>&lt;/red_flags&gt;</p>\n<hr>\n<p>&lt;critical_reminders&gt;</p>\n<h2>CRITICAL REMINDERS</h2>\n<blockquote>\n<p><strong>All code must follow project conventions in CLAUDE.md</strong> (kebab-case, named exports, import ordering, <code>import type</code>, named constants)</p>\n</blockquote>\n<p><strong>(You MUST register every command in <code>tauri::generate_handler![]</code> -- unregistered commands compile fine but silently fail at runtime)</strong></p>\n<p><strong>(You MUST implement <code>serde::Serialize</code> on all error types returned from commands -- Tauri serializes errors across the IPC boundary)</strong></p>\n<p><strong>(You MUST wrap mutable managed state in <code>Mutex</code> or <code>RwLock</code> -- commands run concurrently and <code>State&lt;T&gt;</code> requires <code>Send + Sync</code>)</strong></p>\n<p><strong>(You MUST return <code>Result&lt;T, E&gt;</code> from async commands that use borrowed args (<code>&amp;str</code>, <code>State&lt;'_, T&gt;</code>) -- Rust lifetime rules require it)</strong></p>\n<p><strong>(You MUST use <code>Channel&lt;T&gt;</code> for streaming data to frontend -- events are designed for small payloads, not high-throughput streaming)</strong></p>\n<p><strong>Failure to follow these rules will cause silent command failures, runtime panics, deadlocked async runtimes, or unserializable error types.</strong></p>\n<p>&lt;/critical_reminders&gt;</p>\n","files":[{"path":"examples/core.md","sizeBytes":11520,"isText":true},{"path":"examples/events.md","sizeBytes":5960,"isText":true},{"path":"examples/testing.md","sizeBytes":6238,"isText":true},{"path":"reference.md","sizeBytes":7587,"isText":true},{"path":"SKILL.md","sizeBytes":16533,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-29T15:29:46.102863Z","sha256":"1C58E4E84A38E75D58A6E1F7606AC396E8A96659D6C00DA6B0B59DEFD1FF1999","sizeBytes":16856},"review":null,"source":{"repositoryUrl":"https://github.com/agents-inc/skills","path":"dist/plugins/desktop-backend-tauri/skills/desktop-backend-tauri","license":"MIT","commit":"3a51ef571e996b18294bf776d53dbdad26de0617","subtreeSha":"2A40524B62D61667FCFE8BD5F85F79D4D40FFDE10CC0E17F6EABA52235EA63AD","lastSyncedAt":"2026-09-29T15:27:48.914434Z"},"reviewedAt":"2026-09-29T15:33:47.369505Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/desktop-backend-tauri/skills/desktop-backend-tauri"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart"},{"target":"git","command":"git clone https://github.com/agents-inc/skills.git"}]}