Tauri 2 Structured Error Serialization Produces [object Object]
Tauri 2 Structured Error Serialization Produces [object Object]
Problem
Tauri 2 command errors appear as [object Object] in the UI instead of human-readable messages. Users see no useful
error information when backend commands fail, making it impossible to diagnose issues.
Symptoms:
- Error dialogs show
[object Object]instead of meaningful messages - Backend commands appear to succeed silently (no error shown)
console.logof caught errors shows{ type: "NoWorkspace" }or{ type: "Internal", data: { message: "..." } }- Operations like Create Page appear to do nothing with no feedback
Investigation
Steps Tried
- Checked backend tests - All 218 Rust tests passed, ruling out backend logic bugs
- Inspected Tauri command registration - Commands correctly registered in
invoke_handler - Reviewed frontend catch blocks - Found the pattern
err instanceof Error ? err.message : String(err)used in every component - Tested error serialization - Confirmed Tauri 2 serializes
Result::Erras plain JS objects, notErrorinstances
Root Cause
Tauri 2 uses serde to serialize Rust error types into JavaScript. When a command returns Err(CommandError), the error
is serialized as a plain JS object matching the Rust enum’s serde configuration:
#[derive(Debug, Clone, Serialize, Type)]#[serde(tag = "type", content = "data")]pub enum CommandError { NotFound { entity: String, id: String }, Validation { message: String }, Internal { message: String }, NoWorkspace,}This produces JS objects like:
{ type: "NoWorkspace" }(no data field){ type: "Internal", data: { message: "some error" } }{ type: "NotFound", data: { entity: "Page", id: "abc" } }
The frontend pattern err instanceof Error returns false for these objects because they are not Error instances.
The fallback String(err) calls .toString() which produces "[object Object]" for plain objects.
Solution
1. Create a shared error parser
File: apps/desktop/src-react/lib/errors.ts
export function parseCommandError(err: unknown): string { if (err instanceof Error) return err.message; if (typeof err === "string") return err;
if (typeof err === "object" && err !== null) { const obj = err as Record<string, unknown>;
// CommandError::NoWorkspace if (obj.type === "NoWorkspace") return "No workspace is currently open";
// CommandError variants with data field if (obj.data && typeof obj.data === "object") { const data = obj.data as Record<string, unknown>; if (typeof data.message === "string") return data.message; if (typeof data.entity === "string" && typeof data.id === "string") return `${data.entity} not found: ${data.id}`; }
try { return JSON.stringify(err); } catch { /* ignore */ } }
return "An unexpected error occurred";}2. Replace all catch blocks
Replace every instance of:
err instanceof Error ? err.message : String(err)With:
parseCommandError(err)Implementation Notes
- The parser must match the Rust enum’s
#[serde(tag = "type", content = "data")]layout - Import paths differ for components vs hooks vs nested directories
- Add
console.errorwith the raw error object for diagnostics in critical paths - The
JSON.stringifyfallback handles any unexpected error shapes
Prevention
Best Practices
- Never use
String(err)for Tauri command errors. Always use a structured parser. - Always log the raw error object with
console.errorbefore parsing, so the original shape is visible in dev tools. - Match your parser to your Rust error enum’s serde config. If the enum changes, update the parser.
- When adding new
CommandErrorvariants in Rust, updateparseCommandErrorto handle them.
Warning Signs
- Any catch block using
err instanceof Error ? err.message : String(err)with Tauri invoke calls - Error messages showing
[object Object]in the UI - User-facing errors that say “undefined” or are blank
Search Pattern
Find broken error handling with:
grep -r "instanceof Error ? err.message : String(err)" apps/desktop/src-react/Secondary Issue: Missing Backend Logs
tracing_subscriber::EnvFilter::from_default_env() defaults to off when RUST_LOG is not set. In dev mode, no
backend logs appear without explicit configuration. Fix by falling back to info in debug builds:
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| { if cfg!(debug_assertions) { tracing_subscriber::EnvFilter::new("info") } else { tracing_subscriber::EnvFilter::new("warn") } });References
- INK-50: Create Page does not persist to filesystem
- Branch:
matt/ink-50-create-page-does-not-persist-to-filesystem - Tauri 2 command error handling docs
- Rust serde tag+content enum representation:
#[serde(tag = "type", content = "data")]
Supabase Branch Migration Deployment: CLI vs GitHub Integration Next
Tauri IPC Named-Parameter Wrapping in HTTP Bridge Handlers
Was this page helpful?
Thanks for your feedback!