Saltearse al contenido

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 HTTPde error.codedel body
Body fuera de contrato sobre un status HTTP conocido (p. ej. HTML 502 de nginx, body vacío)del HTTPderivado 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 mapeadodel HTTPUNKNOWN
Fallo de red (fetch rechazado — DNS, TLS, offline)0NETWORK_ERRORError 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.parse sobre error.message. Ahora es texto plano legible.
  • Usa isApiClientError en lugar de instanceof Error. Estrecha el tipo y es seguro entre realms.
  • Ramifica por .code, no por el contenido del message.
  • Usa .requestId cuando reportes una incidencia — correlaciona con nuestros logs.
  • Maneja NETWORK_ERROR explícitamente si te importan los fallos offline / DNS / TLS. Antes lanzaban un TypeError crudo de fetch; ahora aparecen como ApiClientError(status: 0, code: 'NETWORK_ERROR') con el error original preservado en .cause.