C.W.K.
Stream
Lesson 06 of 07 · published

Errors Across the Boundary

~14 min · tauri, errors, result, rust

Level 0Web Tourist
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Ok becomes resolve. Err becomes reject. Model your failures as Result and the frontend gets clean, catchable errors for free."

Result Is the Whole Mechanism

A command that can fail returns Result<T, E>. When it returns Ok(value), the frontend Promise resolves with value. When it returns Err(e), the Promise rejects with e, so a try/catch or .catch() on the JS side receives it. There's no separate error channel to learn — the type you already know is the error protocol.

The Error Type Must Serialize

Because the error crosses the bridge, E has to be serializable. The quick path is Result<T, String> — turn any failure into a message with .map_err(|e| e.to_string()). The robust path is a custom error enum that derives Serialize, so the frontend can distinguish kinds of failure (not-found vs permission-denied vs network) and react differently. Crates like thiserror make defining such an error type a few lines.

The ? Operator Keeps It Clean

Inside a command returning Result, the ? operator unwraps an Ok or returns early with the Err — converting the error along the way if a From impl exists. This turns nested match statements into linear, readable code. With a custom error enum and thiserror's #[from], a chain of fallible operations reads top to bottom with a ? after each.

Code

A typed, serializable error·rust
use serde::Serialize;
use thiserror::Error;

// A custom error the frontend can branch on. derive(Serialize) lets it cross.
#[derive(Debug, Error, Serialize)]
#[serde(tag = "kind", content = "message")]
enum NoteError {
    #[error("note {0} not found")]
    NotFound(u32),
    #[error("disk error: {0}")]
    Io(String),
}

#[tauri::command]
fn get_note(id: u32) -> Result<String, NoteError> {
    let raw = std::fs::read_to_string(format!("notes/{id}.txt"))
        .map_err(|e| NoteError::Io(e.to_string()))?; // ? early-returns on Err
    if raw.is_empty() {
        return Err(NoteError::NotFound(id));
    }
    Ok(raw)
}
Catching it on the web side·tsx
// On the frontend, Err arrives as a rejected Promise.
try {
  const note = await invoke<string>("get_note", { id: 99 });
} catch (err) {
  // err is the serialized NoteError: { kind: "NotFound", message: 99 }
  console.error("could not load note:", err);
}

External links

Exercise

Write a command that returns Result<String, String> and fails on some input (e.g. an empty argument → Err). Call it from the frontend inside a try/catch and render the error message in the UI. Bonus: upgrade the error to a small enum with derive(Serialize) and branch the UI on the error kind.
Hint
Simplest fail: if name.trim().is_empty() { return Err("name required".into()); }. On the frontend, the catch block's err is exactly what you put in Err. For the bonus, #[serde(tag="kind")] gives you a discriminable shape in TS.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.