Errores y códigos de estado
Errores y códigos de estado
htmx 4 cambió el comportamiento por defecto ante errores, y añadió un atributo dedicado para controlarlo con precisión. Es uno de los cambios que más simplifica el código real.
El cambio: los errores ahora hacen swap
| Versión | Comportamiento ante 4xx/5xx |
|---|---|
| htmx 2 | No se hace swap; hay que interceptar htmx:beforeSwap |
| htmx 4 | Se hace swap con todos los códigos excepto 204 y 304 |
En htmx 2, devolver un 422 con el HTML de los errores de validación no mostraba nada, y todo el
mundo terminaba escribiendo el mismo listener global para permitirlo. htmx 4 asume el caso común:
si el servidor se tomó el trabajo de devolver HTML, es porque quiere que se vea.
Para restaurar el comportamiento anterior:
htmx.config.noSwap = [204, 304, '4xx', '5xx'];
noSwap acepta códigos exactos y comodines.
hx-status: comportamiento por código
Nuevo en htmx 4. Permite declarar qué hacer con cada código HTTP directamente en el elemento:
<form hx-post="/guardar"
hx-target="#formulario"
hx-status:422="swap:innerHTML target:#errores select:#validacion"
hx-status:5xx="swap:none push:false">
…
</form>
Formato del selector de código
| Patrón | Coincide con |
|---|---|
404 | Exactamente 404 |
50x | 500–509 |
4xx | 400–499 |
5xx | 500–599 |
Se evalúan en orden de especificidad: un hx-status:404 gana sobre un hx-status:4xx.
Claves disponibles
| Clave | Qué controla |
|---|---|
swap | Estrategia de swap para ese código |
target | Destino para ese código |
select | Qué parte de la respuesta usar |
push | Si se empuja la URL al historial |
replace | Si se reemplaza la URL |
transition | Si se usa View Transition |
Ejemplo completo
<form id="alta-usuario"
hx-post="/usuarios"
hx-target="#lista"
hx-swap="beforeend"
hx-status:422="target:#alta-usuario swap:outerMorph"
hx-status:409="target:#avisos swap:afterbegin"
hx-status:401="target:body swap:innerHTML"
hx-status:5xx="target:#avisos swap:afterbegin">
<input name="email">
<button type="submit">Crear</button>
</form>
Lectura directa: si todo va bien, la respuesta se añade a la lista; si hay error de validación, el formulario se recompone con morphing; si hay conflicto o error de servidor, aparece un aviso arriba; si expiró la sesión, se pinta la página de login completa.
Todo eso sin una línea de JavaScript.
flowchart TD
R[Respuesta del servidor] --> C{Código}
C -->|200| A["#lista beforeend"]
C -->|422| B["#alta-usuario outerMorph"]
C -->|409| D["#avisos afterbegin"]
C -->|401| E["body innerHTML"]
C -->|5xx| F["#avisos afterbegin"]
C -->|204 / 304| G[sin swap]
Convenciones de códigos en una app htmx
| Código | Cuándo usarlo | Qué devolver |
|---|---|---|
200 | Éxito con contenido | El fragmento actualizado |
201 | Recurso creado | El fragmento del recurso nuevo |
204 | Éxito sin cambios visibles | Nada |
400 | Petición malformada | Mensaje de error |
401 | No autenticado | Formulario de login o HX-Redirect |
403 | Sin permisos | Mensaje |
404 | No existe | Mensaje o estado vacío |
409 | Conflicto (duplicado, versión) | Mensaje con la opción de resolver |
422 | Validación fallida | El formulario con los errores |
5xx | Error del servidor | Mensaje genérico |
204 es especialmente útil: “hice lo que pediste y no hay nada que actualizar”. Es lo correcto
para un DELETE cuando el elemento se quita con hx-swap="delete".
Headers de respuesta que alteran el comportamiento
El servidor puede sobrescribir casi cualquier decisión del cliente.
| Header | Efecto |
|---|---|
HX-Retarget | Cambia el destino del swap |
HX-Reswap | Cambia la estrategia (acepta modificadores) |
HX-Reselect | Cambia qué parte de la respuesta se usa |
HX-Trigger | Dispara eventos en el cliente |
HX-Push-Url | Empuja una URL al historial |
HX-Replace-Url | Reemplaza la URL actual |
HX-Location | Navegación del lado cliente sin recarga |
HX-Redirect | Redirección completa del navegador |
HX-Refresh | true fuerza recarga total de la página |
Redirigir el error a otro sitio
if (!usuario) {
return new Response(`<p class="error">Tu sesión expiró</p>`, {
status: 401,
headers: {
"Content-Type": "text/html",
"HX-Retarget": "#avisos",
"HX-Reswap": "afterbegin",
},
});
}
Esto evita tener que anticipar en el HTML todos los destinos posibles de error.
HX-Redirect vs HX-Location
// Recarga completa del navegador
headers: { "HX-Redirect": "/login" }
// Navegación del lado cliente: htmx pide la URL y hace swap
headers: { "HX-Location": "/tareas" }
// Con opciones en HCON
headers: { "HX-Location": 'path:"/tareas" target:"#main" swap:"innerHTML"' }
Usa HX-Redirect cuando el estado del navegador debe reiniciarse (login, logout). Usa
HX-Location cuando quieres navegar sin perder lo que ya está cargado.
Notificar eventos desde el error
return new Response(fragmento, {
status: 409,
headers: {
"Content-Type": "text/html",
"HX-Trigger": '{"mostrarToast": {"tipo": "error", "texto": "Ya existe ese RUT"}}',
},
});
En el cliente:
<div id="toasts"
hx-on:mostrarToast="agregarToast(event.detail.tipo, event.detail.texto)">
</div>
Timeouts
htmx 4 tiene un timeout por defecto de 60 000 ms (en htmx 2 era 0, sin límite).
<!-- Global -->
<meta name="htmx-config" content="defaultTimeout:10000">
<!-- Por subárbol o elemento -->
<button hx-post="/informe-pesado" hx-config="timeout:300000">Generar</button>
Cuando se agota, htmx emite htmx:error (y htmx:abort). Manejo global:
htmx.on("htmx:error", () => {
mostrarToast("La operación tardó demasiado o no hay conexión.");
});
Errores de red
Un error de red (servidor caído, sin conexión) no produce una respuesta HTTP, así que no lo captura
hx-status. Se maneja con el evento:
htmx.on("htmx:error", (e) => {
document.querySelector("#estado-conexion").textContent = "Sin conexión";
});
htmx.on("htmx:after:request", () => {
document.querySelector("#estado-conexion").textContent = "";
});
flowchart TD
P[Petición] --> Q{¿Hubo respuesta HTTP?}
Q -->|No: red caída, timeout| E["htmx:error"]
Q -->|Sí| S{¿Código?}
S -->|2xx| OK["htmx:after:request + swap"]
S -->|4xx / 5xx| RE["htmx:response:error + swap<br/>+ reglas de hx-status"]
Página de error genérica
Un patrón robusto: una zona de avisos en el layout y una regla global de hx-status heredada.
<body hx-status:5xx:inherited="target:#avisos swap:afterbegin"
hx-status:401:inherited="target:body swap:innerHTML">
<div id="avisos" aria-live="polite"></div>
<main id="contenido">…</main>
</body>
Cualquier petición de la aplicación que falle con 500 pinta el aviso en el mismo sitio, y cualquier 401 reemplaza la página con el login. Sin duplicar nada.
Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| El 422 no muestra los errores | Config heredada de htmx 2 en noSwap | Revisar htmx.config.noSwap |
| El 500 pinta HTML feo del framework | El servidor devuelve su página de error | Devolver un fragmento propio para peticiones con HX-Request |
| El timeout corta subidas grandes | Default de 60 s | hx-config="timeout:…" en ese elemento |
HX-Redirect no hace nada | Se envió junto con un 204 que no se procesa | Usar 200 con cuerpo vacío |
| El error se inserta donde no corresponde | No hay regla por código | hx-status:<código> o HX-Retarget |
Resumen
- htmx 4 hace swap con todos los códigos salvo
204y304. hx-status:<código>declara target, swap, select y push por código HTTP.- Los patrones aceptan
404,50x,4xx,5xx, evaluados por especificidad. HX-Retarget,HX-ReswapyHX-Reselectpermiten al servidor decidir el destino.HX-Redirectrecarga;HX-Locationnavega del lado cliente.- El timeout por defecto es de 60 s y se ajusta con
hx-config="timeout:…". - Los errores de red no tienen código: se manejan con el evento
htmx:error.
Siguiente: Historial y boost →.