Saltar al contenido

Por qué devolver una cadena no sirve

El error más fácil de escribir en la frontera y el que peor envejece. Las cinco cosas que el frontend no puede hacer con un mensaje de texto, y por qué el problema empeora con el tiempo en vez de estabilizarse.

Lo que el frontend no puede hacer

Rust
#[tauri::command]async fn cargar_proyecto(id: ProyectoId) -> Result<Proyecto, String> {    logica::cargar(id).await.map_err(|e| e.to_string())}
JS
catch (e) {  // e es una cadena. Y ahora, ¿qué?}

Por qué empeora con el tiempo

Es lo que distingue esta decisión de otras que son simplemente subóptimas:

Los mensajes cambian. Una errata corregida, una frase mejorada, un detalle añadido. Cada cambio rompe cualquier comparación que alguien hubiera escrito, y no da error de compilación en ninguno de los dos lados: simplemente el if deja de entrar.

Los mensajes se anidan. Un error de entrada y salida convertido a cadena dentro de un error de tu lógica convertido a cadena produce frases del tipo «no se pudo cargar el proyecto: error al abrir: No such file or directory (os error 2)». Legibles para ti y para nadie más.

Los casos se multiplican. Con tres errores posibles la gente los distingue mirando; con treinta, nadie lo hace y todo acaba en «algo ha salido mal», que es el antipatrón del nivel 13.3.

Y llega la traducción. El día que la aplicación tiene que hablar dos idiomas, un error que es una frase en español no tiene arreglo barato: hay que reescribir la frontera entera para tener claves.

Los tres síntomas

Si tu proyecto ya está aquí, se reconoce por:

  1. Comparaciones de texto en el frontend. if (e.includes('no existe')). Es deuda con fecha de caducidad desconocida.
  2. Un catch que enseña el error del core tal cual. El usuario ve un mensaje escrito para un programador, a veces con una ruta absoluta dentro.
  3. Ningún error se trata de forma distinta. Todo acaba en el mismo aviso, porque distinguir era demasiado trabajo.

La forma mínima que lo arregla

No hace falta un sistema elaborado. Con dos campos se resuelven las cinco filas de la tabla:

Rust
#[derive(serde::Serialize)]#[serde(rename_all = "camelCase")]pub struct ErrorPayload {    /// estable, en mayúsculas, para que el frontend decida    pub code: String,    /// para el registro y para el desarrollador, no para el usuario    pub message: String,}

Y con eso el frontend puede hacer lo del nivel 13.3:

JS
catch (e) {  if (e.code === 'NO_EXISTE') return avisar(t('proyecto.no_existe'));  if (e.code === 'RED')       return reintentarMasTarde();  registrar(e.code, e.message);  avisar(t('generico'));}

Los dos niveles siguientes desarrollan las dos mitades: el 18.2 es cómo se produce ese payload sin ensuciar la lógica, y el 18.4 es cómo se diseñan los códigos para que no acaben siendo treinta cadenas sin criterio.