Rust to TypeScript error handling for Tauri apps. Use when mentioning Rust errors, Tauri command errors, invoke errors, or defining Rust error types for TS consumption.
65
77%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Fix and improve this skill with Tessl
tessl review fix ./.agents/skills/rust-errors/SKILL.mdWhen Rust error serialization, Tauri command error transport, IPC payload shape, generated bindings, or frontend invoke error behavior affects correctness, use source-backed grounding before relying on memory. If DeepWiki MCP is available, ask a narrow question against tauri-apps/tauri; if it is unavailable or the repo is not indexed, use upstream source or official docs directly. Treat DeepWiki as orientation, then verify decisive details against local Rust code, generated bindings, installed crates, TypeScript types, source, or official docs before changing code.
Skip DeepWiki for local error naming conventions already documented below.
When passing errors from Rust to TypeScript through Tauri commands, use internally-tagged enums to create discriminated unions that TypeScript can handle naturally.
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug, Serialize, Deserialize)]
#[serde(tag = "name")]
pub enum TranscriptionError {
#[error("Audio read error: {message}")]
AudioReadError { message: String },
#[error("GPU error: {message}")]
GpuError { message: String },
#[error("Model load error: {message}")]
ModelLoadError { message: String },
#[error("Transcription error: {message}")]
TranscriptionError { message: String },
}#[serde(tag = "name")] creates a discriminator fieldmessage: String// Single-variant enum for consistency
#[derive(Error, Debug, Serialize, Deserialize)]
#[serde(tag = "name")]
enum ArchiveExtractionError {
#[error("Archive extraction failed: {message}")]
ArchiveExtractionError { message: String },
}import { type } from 'arktype';
// Define the error type to match Rust serialization
const TranscriptionErrorType = type({
name: "'AudioReadError' | 'GpuError' | 'ModelLoadError' | 'TranscriptionError'",
message: 'string',
});
// Use in error handling
const result = await tryAsync({
try: () => invoke('transcribe_audio_whisper', params),
catch: (unknownError) => {
const result = TranscriptionErrorType(unknownError);
if (result instanceof type.errors) {
// Handle unexpected error shape
return WhisperingErr({
title: 'Unexpected Error',
description: extractErrorMessage(unknownError),
action: { type: 'more-details', error: unknownError },
});
}
const error = result;
// Now we have properly typed discriminated union
switch (error.name) {
case 'ModelLoadError':
return WhisperingErr({
title: 'Model Loading Error',
description: error.message,
action: {
type: 'more-details',
error: new Error(error.message),
},
});
case 'GpuError':
return WhisperingErr({
title: 'GPU Error',
description: error.message,
action: {
type: 'link',
label: 'Configure settings',
href: '/settings/transcription',
},
});
// Handle other cases...
}
},
});The Rust enum serializes to this TypeScript-friendly format:
// AudioReadError variant
{ "name": "AudioReadError", "message": "Failed to decode audio file" }
// GpuError variant
{ "name": "GpuError", "message": "GPU acceleration failed" }name and messagecontent attribute: Avoid #[serde(tag = "name", content = "data")] as it creates nested structuresFor Tauri commands that generate TypeScript bindings:
Serialize, Deserialize when the value crosses the IPC boundary both ways.specta::Type for command inputs, outputs, and event payloads that appear in generated bindings.#[serde(rename = "...")].thiserror; do not make TypeScript reconstruct Rust context from separate fields unless the UI needs structured handling.Generated bindings are a contract check, not just output. If a Rust change should alter the TypeScript command or event surface, regenerate bindings and review the generated diff. If the generated diff is large but the public IPC shape did not change, stop and find why before committing it.
// DON'T: External tagging (default behavior)
#[derive(Serialize)]
pub enum BadError {
ModelLoadError { message: String }
}
// Produces: { "ModelLoadError": { "message": "..." } }
// DON'T: Adjacent tagging with content
#[derive(Serialize)]
#[serde(tag = "type", content = "data")]
pub enum BadError {
ModelLoadError { message: String }
}
// Produces: { "type": "ModelLoadError", "data": { "message": "..." } }
// DON'T: Manual Serialize implementation when derive works
impl Serialize for MyError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> {
// Unnecessary complexity
}
}This pattern ensures clean, type-safe error handling across the Rust-TypeScript boundary with minimal boilerplate and maximum type safety.
tracing ↔ wellcrafted/loggerdefineErrors mirrors thiserror; the workspace logger mirrors tracing. Together they give TypeScript the same split Rust has: errors are data, level is chosen at the emit site.
fatal)tracing macro | Workspace Logger method | Use when |
|---|---|---|
tracing::trace!(...) | log.trace(message, data?) | Per-token / per-message noise for deep debugging |
tracing::debug!(...) | log.debug(message, data?) | Internal state transitions (handshakes, cache fills) |
tracing::info!(...) | log.info(message, data?) | Lifecycle events (connected, loaded, flushed) |
tracing::warn!(?err) | log.warn(err) | Recoverable failure: retry path, fallback taken |
tracing::error!(?err) | log.error(err) | Unrecoverable at this layer: call it loudly |
tracing has no fatal; neither do we. Process termination is the app's decision (process.exit), not the library's.
// Rust: level is on the CALL, not the enum variant
tracing::warn!(?err, "cache miss"); // same err, different sites
tracing::error!(?err, "giving up");// TS: same rule
log.warn(CacheError.Miss({ key })); // recoverable
log.error(CacheError.Miss({ key })); // terminalNo Rust logging crate attaches level to the error type (thiserror, anyhow, slog, log). miette is the exception, but it is a compiler-diagnostics library, not a general logger. We follow tracing: level is context, not identity.
?err idiom ↔ tapErrtracing's ?err interpolates a structured error field into the log event. In TS, the Result-flow equivalent is tapErr (from wellcrafted/result):
let result = do_thing().inspect_err(|err| tracing::warn!(?err, "do_thing failed"));import { tapErr } from 'wellcrafted/result';
const result = await tryAsync({
try: () => doThing(),
catch: (cause) => DoThingError.Failed({ cause }),
}).then(tapErr(log.warn));Both: pass-through on success, log the structured error on failure.
In practice this shape is rare in epicenter. Most call sites need the Ok data locally and so branch on result.error and log inside the branch. Reach for tapErr only when the Result flows out of the function in a .then(...) chain.
cb12bcc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.