Saltar al contenido

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

Rust
#[derive(Debug, serde::Serialize)]#[serde(rename_all = "camelCase")]pub struct ErrorPayload {    pub code: String,    pub message: String,}
  • code es 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.
  • message es 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í:

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

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

JS
const esPayload = (e) => e && typeof e === 'object' && typeof e.code === 'string';