Manejo de errores
Todos los métodos del cliente (getContent, getContentList, etc.) lanzan un ApiClientError cuando la petición falla. El error es una subclase tipada de Error que refleja el contrato de error de la API REST y añade códigos adicionales para fallos del lado del cliente.
ApiClientError
import { ApiClientError, isApiClientError } from '@content-island/api-client';
class ApiClientError extends Error { readonly status: number; // código HTTP (0 si la petición no llegó al servidor) readonly code: ApiClientErrorCode; readonly requestId?: string; // presente cuando el servidor devolvió el body contractual readonly details?: Record<string, unknown>; // .message es el texto legible del servidor (o el fallback sintetizado) // .cause se rellena cuando se envuelve un fallo de red (TypeError de fetch)}
const isApiClientError: (error: unknown) => error is ApiClientError;ApiClientErrorCode
type ApiClientErrorCode = | 'VALIDATION_ERROR' // 400 | 'UNAUTHORIZED' // 401 | 'FORBIDDEN' // 403 | 'NOT_FOUND' // 404 | 'CONFLICT' // 409 | 'INTERNAL_ERROR' // 5xx | 'NETWORK_ERROR' // la petición no llegó al servidor (status: 0) | 'UNKNOWN' // respuesta fuera del contrato sobre un status no clasificable | 'SNAPSHOT_MODE' // método de escritura/gestión llamado en un cliente en modo snapshot (sin petición de red) | 'RATE_LIMITED'; // límite de peticiones por proyecto del endpoint de exportación superado (429)Los seis primeros vienen directamente del contrato de la API REST. NETWORK_ERROR y UNKNOWN los sintetiza el cliente cuando la respuesta del servidor no se puede parsear (ver Fallback elegante). SNAPSHOT_MODE se lanza cuando se llama a un método de escritura o gestión sobre un cliente en modo snapshot — las escrituras no están disponibles en modo snapshot, así que la llamada se rechaza sin realizar ninguna petición de red. RATE_LIMITED se lanza cuando se supera el límite de peticiones por proyecto del endpoint de exportación (HTTP 429); aparece desde exportSnapshot / la CLI content-island export.
Patrón recomendado
import { isApiClientError } from '@content-island/api-client';
try { const post = await client.getContent<Post>({ id: 'abc123' });} catch (error) { if (!isApiClientError(error)) { throw error; } switch (error.code) { case 'NOT_FOUND': // pintar un estado vacío break; case 'UNAUTHORIZED': // refrescar / rotar el token de acceso break; case 'VALIDATION_ERROR': { const fields = error.details?.fields as Array<{ field: string; message: string }> | undefined; // mostrar los campos al usuario break; } case 'NETWORK_ERROR': // reintentar / mostrar UI offline break; default: // loguear y reportar — incluye error.requestId para que soporte pueda rastrear la llamada console.error(`[${error.code}] ${error.message}`, error.requestId); }}Fallback elegante
El cliente garantiza que cualquier fallo aparece como un ApiClientError, incluso cuando la respuesta del servidor no sigue el contrato.
| Origen del fallo | .status | .code | .requestId | .cause |
|---|---|---|---|---|
| Body de error contractual (el caso normal) | del HTTP | de error.code | del body | — |
Body fuera de contrato sobre un status HTTP conocido (p. ej. HTML 502 de nginx, body vacío) | del HTTP | derivado del HTTP — 400→VALIDATION_ERROR, 401→UNAUTHORIZED, 403→FORBIDDEN, 404→NOT_FOUND, 409→CONFLICT, 422→VALIDATION_ERROR, 5xx→INTERNAL_ERROR | — | — |
| Body fuera de contrato sobre un status HTTP no mapeado | del HTTP | UNKNOWN | — | — |
Fallo de red (fetch rechazado — DNS, TLS, offline) | 0 | NETWORK_ERROR | — | Error original |
En los casos sintetizados .message cae al statusText HTTP (o Network request failed para rechazos de fetch), así que sigue siendo seguro loguearlo.
Migración desde v0.20.x
@content-island/api-client@0.21.0 introduce el error tipado. Es un cambio incompatible — los métodos antes lanzaban un Error plano cuyo message era un payload serializado como JSON, y ahora lanzan un ApiClientError tipado.
Antes (v0.20.x)
try { await client.getContent({ id: 'abc' });} catch (error) { // error era un Error plano con el message serializado como JSON const parsed = JSON.parse((error as Error).message); parsed.status; // 404 parsed.statusText; // "Not Found" parsed.body; // body crudo}Después (v0.21.0)
import { isApiClientError } from '@content-island/api-client';
try { await client.getContent({ id: 'abc' });} catch (error) { if (isApiClientError(error)) { error.status; // 404 error.code; // 'NOT_FOUND' error.message; // legible, del servidor error.requestId; // coincide con el log del servidor error.details; // { fields: [...] } en VALIDATION_ERROR }}Qué cambia en la práctica:
- Deja de hacer
JSON.parsesobreerror.message. Ahora es texto plano legible. - Usa
isApiClientErroren lugar deinstanceof Error. Estrecha el tipo y es seguro entre realms. - Ramifica por
.code, no por el contenido del message. - Usa
.requestIdcuando reportes una incidencia — correlaciona con nuestros logs. - Maneja
NETWORK_ERRORexplícitamente si te importan los fallos offline / DNS / TLS. Antes lanzaban unTypeErrorcrudo defetch; ahora aparecen comoApiClientError(status: 0, code: 'NETWORK_ERROR')con el error original preservado en.cause.