Result y el operador de propagación
Cómo un comando dice que ha fallado, qué exige el tipo de error, y por qué el operador que propaga errores en Rust es lo que hace que la frontera del nivel 14.5 quepa en tres líneas.
La firma
#[tauri::command]async fn cargar_proyecto(id: ProyectoId) -> Result<Proyecto, ErrorPayload> { … }Lo que eso significa en cada lado:
El operador de propagación
Es lo que evita que el cuerpo del comando se llene de match:
// Sin el operador: cada fallo, tratado a mano.#[tauri::command]async fn cargar_proyecto(app: AppHandle, id: ProyectoId) -> Result<Proyecto, ErrorPayload> { let base = match app.path().app_data_dir() { Ok(b) => b, Err(e) => return Err(ErrorPayload::from(e)), }; let p = match logica::cargar(&base, id).await { Ok(p) => p, Err(e) => return Err(ErrorPayload::from(e)), }; Ok(p)}// Con el operador: lo mismo, en tres líneas.#[tauri::command]async fn cargar_proyecto(app: AppHandle, id: ProyectoId) -> Result<Proyecto, ErrorPayload> { let base = app.path().app_data_dir()?; Ok(logica::cargar(&base, id).await?)}El ? hace exactamente esto: si es correcto, saca el valor; si es un fallo, convierte el error al tipo de retorno y vuelve inmediatamente. La palabra clave es convierte: no exige que los errores sean del mismo tipo, sino que exista una conversión.
Las conversiones que hacen falta
Ahí está el trabajo, y es poco:
// error.rs#[derive(serde::Serialize)]#[serde(rename_all = "camelCase")]pub struct ErrorPayload { pub code: String, pub message: String,}// Una conversión por cada tipo de error que quieras propagar con ?impl From<std::io::Error> for ErrorPayload { fn from(e: std::io::Error) -> Self { let code = match e.kind() { std::io::ErrorKind::NotFound => "NO_EXISTE", std::io::ErrorKind::PermissionDenied => "SIN_PERMISO", _ => "IO", }; ErrorPayload { code: code.into(), message: e.to_string() } }}impl From<tauri::Error> for ErrorPayload { … }impl From<crate::proyectos::ProyectoError> for ErrorPayload { … }Con esas conversiones escritas una vez, todos los comandos pueden usar ? sobre cualquiera de esos errores. Es la razón de que la capa de frontera del nivel 14.5 pueda ser de tres líneas: el trabajo de traducir está en un sitio y no en cada comando.
Cuándo un fallo no debe ser un error
Tres casos frecuentes en los que devolver Err es la respuesta equivocada:
Cuando la ausencia es normal. Buscar algo y no encontrarlo no es un fallo: es un resultado. Ok(None) describe mejor «no hay» que Err(NO_EXISTE), y evita que el frontend tenga que meter el caso normal en un catch. La regla: si el usuario no ha hecho nada mal y el sistema funciona, probablemente es un Ok.
Cuando el resultado es parcial. Procesar cien ficheros y que fallen tres no es un fallo del comando: es un resultado con detalle. Devolver Ok con la lista de logrados y la de fallidos le da al frontend lo que necesita para enseñar algo útil; devolver Err tira los noventa y siete que sí salieron.
Cuando es un error de programación. Un estado imposible, una invariante rota. Eso no es un Err que el frontend deba manejar —¿qué va a hacer el usuario?— sino un pánico o un registro. Es el caso 6 del nivel 13.3.