Saltar al contenido

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

Rust
#[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:

Rust
// 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:

Rust
// 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.