"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.