rust-errors
Maps Rust errors to TypeScript discriminated unions for Tauri apps.
Install
mkdir -p .claude/skills/rust-errors && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/1548" && unzip -o skill.zip -d .claude/skills/rust-errors && rm skill.zipInstalls to .claude/skills/rust-errors
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
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.Key capabilities
- →Define internally-tagged Rust enums for serialization
- →Map Rust error variants to TypeScript discriminated unions
- →Validate IPC payloads using arktype
- →Generate type-safe bindings for Tauri commands
- →Implement consistent error logging levels
- →Handle async command errors in frontend code
How it works
The skill uses internally-tagged Rust enums to serialize errors into a consistent JSON format that TypeScript consumes as a discriminated union.
Inputs & outputs
When to use rust-errors
- →Defining Rust error types for Tauri commands
- →Mapping Rust error variants to TypeScript discriminated unions
- →Handling async Tauri command errors in frontend code
About this skill
Rust to TypeScript Error Handling
Reference Repositories
- Tauri: Desktop app framework (source of Rust-to-TypeScript error patterns)
Upstream Grounding
When 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.
Discriminated Union Pattern for Errors
When passing errors from Rust to TypeScript through Tauri commands, use internally-tagged enums to create discriminated unions that TypeScript can handle naturally.
Rust Error Definition
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 },
}
Key Rust Patterns
- Use internally tagged enums:
#[serde(tag = "name")]creates a discriminator field - Follow naming conventions: Enum variants should be PascalCase
- Include structured data: Each variant can have fields like
message: String - Single-variant enums are okay: Use when you want consistent error structure
// Single-variant enum for consistency
#[derive(Error, Debug, Serialize, Deserialize)]
#[serde(tag = "name")]
enum ArchiveExtractionError {
#[error("Archive extraction failed: {message}")]
ArchiveExtractionError { message: String },
}
TypeScript Error Handling
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...
}
},
});
Serialization Format
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" }
Best Practices
- Consistent error structure: All errors have the same shape with
nameandmessage - TypeScript type safety: Use runtime validation with arktype to ensure type safety
- Exhaustive handling: Switch statements provide compile-time exhaustiveness checking
- Don't use
contentattribute: Avoid#[serde(tag = "name", content = "data")]as it creates nested structures - Keep enums private when possible: Only make public if used across modules
Tauri Command Surface Rules
For Tauri commands that generate TypeScript bindings:
- Derive
Serialize,Deserializewhen the value crosses the IPC boundary both ways. - Derive
specta::Typefor command inputs, outputs, and event payloads that appear in generated bindings. - Keep the Rust enum variant name aligned with the TypeScript discriminant unless there is a deliberate
#[serde(rename = "...")]. - Keep user-facing message strings on the error variant with
thiserror; do not make TypeScript reconstruct Rust context from separate fields unless the UI needs structured handling. - Register events in the Tauri specta builder even if the event type is not returned by a command.
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.
Anti-Patterns to Avoid
// 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/logger
defineErrors 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.
Level mapping (5 levels, no 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.
Level on the variant? No.
// 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 })); // terminal
No 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.
The ?err idiom ↔ tapErr
tracing'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.
When not to use it
- →When using external or adjacent tagging for serialization
- →When implementing manual serialization logic
Prerequisites
Limitations
- →Requires consistent naming conventions for enum variants
- →Avoids nested structures created by content tagging
How it compares
This method ensures type safety across the IPC boundary by generating bindings and using runtime validation rather than relying on manual string parsing.
Compared to similar skills
rust-errors side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| rust-errors (this skill) | 5 | 1mo | No flags | Advanced |
| hula-skill | 3 | 7mo | Review | Intermediate |
| tauri-syntax-permissions | 0 | 4mo | No flags | Intermediate |
| perf | 0 | 2mo | Review | Advanced |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by EpicenterHQ
View all by EpicenterHQ →You might also like
hula-skill
HuLaSpark
HuLa project skill for frontend (Vue 3 + Vite + UnoCSS + Naive UI/Vant), backend (Tauri v2 + Rust + SeaORM/SQLite), full-stack flows, and build/release work. Use when the user mentions hula or HuLa or requests changes in this repository; after triggering, ask which scope (frontend/backend/fullstack/build-release) to enable.
tauri-syntax-permissions
OpenAEC-Foundation
>
perf
microsoft
Speed and memory performance rules for Rust crates, webui-framework, and webui-router.
add-new-setting-field
tsukumijima
【設定追加時は必ず参照】KonomiTV に新しい設定 (v-switch/v-select など) を追加する際の必須手順。SettingsStore.ts / Settings.ts / config.py / Settings/*.vue への追加が必要
shadmin-dev
ahaodev
Apply Shadmin feature-development standards (backend Go/Gin/Ent + frontend React/TS). Use when adding/modifying features, CRUD modules, API routes/controllers/usecases/repositories, Ent schemas, frontend pages/routes, React components, TanStack hooks, or any full-stack work in this project. Trigger
scaffold-feature
niklasbrandt
Scaffold a complete full-stack feature: FastAPI endpoint, dashboard Web Component, i18n keys, test stubs, and documentation checks.