ErrorPayload: la forma que cruza
Los dos campos obligatorios, los tres opcionales que merecen la pena, y por qué este tipo tiene que ser un struct plano y no el enum de error por muy tentador que sea derivarle la serialización.
Los dos obligatorios
#[derive(Debug, serde::Serialize)]#[serde(rename_all = "camelCase")]pub struct ErrorPayload { pub code: String, pub message: String,}codees para el programa. Estable, en mayúsculas con guiones bajos, y parte del contrato: cambiarlo rompe al frontend igual que cambiar el nombre de un campo.messagees para una persona. Va al registro y al informe de fallo. No se le enseña al usuario tal cual: el usuario ve lo que el frontend decida a partir del código.
Los tres opcionales que merecen la pena
Y el payload completo, para un proyecto que llegue ahí:
#[derive(Debug, serde::Serialize)]#[serde(rename_all = "camelCase")]pub struct ErrorPayload { pub code: String, pub message: String, #[serde(skip_serializing_if = "Option::is_none")] pub retryable: Option<bool>, #[serde(skip_serializing_if = "Option::is_none")] pub details: Option<serde_json::Value>, #[serde(skip_serializing_if = "Option::is_none")] pub trace_id: Option<String>,}Y el tipo del otro lado, que con bindings generados sale solo:
type ErrorPayload = { code: string; message: string; retryable?: boolean; details?: unknown; traceId?: string;};Por qué no serializar el enum directamente
Es la tentación evidente: el enum ya tiene toda la información, ya sabe hacer un mensaje, y con el etiquetado del nivel 17.2 produciría una unión discriminada preciosa. Y aun así no conviene, por cuatro razones:
La indirección cuesta una función From por módulo. Lo que compra es que la forma interna de tus errores y el contrato con el frontend sean dos cosas independientes, y eso permite refactorizar una sin tocar la otra durante los años que dure el proyecto.
El caso especial del pánico
Un comando que entra en pánico —el caso 6 del nivel 13.3— no pasa por tu conversión: el framework lo convierte en un rechazo con su propio formato. Conviene saberlo porque significa que el frontend puede recibir un error que no tiene tu forma.
La defensa es la del nivel 13.3: comprobar que el error tiene la forma esperada antes de leer el código, y tratar lo que no la tenga como un fallo interno.
const esPayload = (e) => e && typeof e === 'object' && typeof e.code === 'string';