Table of Contents
Introduction
htmx is a great library that brings better responsiveness to server-side-rendered websites. I used it in letsblock.it to:
-
retrieve the render preview asynchronously while the user inputs template parameters
-
boost most navigation links to avoid the "flash of white" that some browsers show when switching pages
It does a great job of abstracting the AJAX and DOM swapping logic away, by exposing it through simple HTML attributes. There is still one topic that requires some effort: error handling. On a typical server-side-rendered website, connection errors or request errors trigger the brower's error pages. When htmx is used, these errors are logged to the console, but the user is stuck with a button that has no visible effect.
As the UI designer, you have to decide when and how to surface errors to your users. letsblock.it was hosted on a single VPS without high-availability, so I wanted to surface transient errors caused by the server being unavailable or broken:
Just give me the code
The following example will show an error message to the user in case of a network error or failed request and clear out the message on the next successful request:
- Add a hidden
divto your page layout, with thehtmx-alertID and relevant styles. Here is an example for a bootstrap alert:
<div id="htmx-alert" hidden class="alert alert-warning sticky-top"></div>
- Add the following JS snippet to your frontend code:
document.body.addEventListener('htmx:afterRequest', function (evt) {
const errorTarget = document.getElementById("htmx-alert")
if (evt.detail.successful) {
errorTarget.setAttribute("hidden", "true")
errorTarget.innerText = "";
} else if (evt.detail.failed && evt.detail.xhr) {
console.warn("Server error", evt.detail)
const xhr = evt.detail.xhr;
errorTarget.innerText = `Unexpected server error: ${xhr.status} - ${xhr.statusText}`;
errorTarget.removeAttribute("hidden");
} else {
console.error("Unexpected htmx error", evt.detail)
errorTarget.innerText = "Unexpected error, check your connection and try to refresh the page.";
errorTarget.removeAttribute("hidden");
}
});
Give me the details
The htmx processing flow
Here is a simplified overview of how a simple hx-post form is processed by htmx, setting aside advanced features:
-
When the DOM is loaded,
processNodetraverses the body to find elements withhx-*attributes. Based on the configuration held in these attributes, it generates and attaches an event handler on these elements. -
The generated event handler will check for preconditions before calling
issueAjaxRequest, a function that:- prepares the request, failing with
htmx:targetErrorif the specified target element is not found, orhtmx:invalidPathif the request path is invalid - runs the XHR requests, which can trigger
htmx:sendErrororhtmx:timeoutevents if the request fails without a response from the server. In either case, ahtmx:afterRequestevent is also emitted - if the server returns a valid response, a configurable
responseHandleris called to process it
- prepares the request, failing with
-
In most cases,
handleAjaxResponseis used as theresponseHandler. It:- checks the response code from the server, and emits
htmx:responseErrorif the HTTP code is higher than 400 - modifies the DOM according to the response and the configuration
- checks the response code from the server, and emits
-
After
responseHandlerreturns,issueAjaxRequestemits two events:htmx:afterRequestandhtmx:afterOnLoad. If the handler were to throw an exception, ahtmx:onLoadErrorevent is emitted instead
What events to listen for
For letsblock.it, my goal was to surface errors when the server is unavailable or returning
an error. In my opinion, other errors are not actionable by users, that's why I'm ignoring htmx:targetError,
htmx:invalidPath and htmx:onLoadError events, and not hooking into the undocumented htmx:error event.
These should be caught by testing before the release, and are still visible in the browser console.
My first iteration listened for htmx:sendError and htmx:responseError to show the alert, but it could break if
other error events were added in future releases. Case in point, I initially forgot to listen for htmx:timeout,
and didn't surface these. As I already listened for htmx:afterRequest to hide the alert, I decided to only listen
for that event that is always emitted.
A couple of pitfalls
-
The documentation for htmx:afterRequest fails not mention an important pitfall: the
detail.successfulanddetail.failedare set byhandleAjaxResponse, which means that they will NOT be set if htmx fails before a request is received from the server, or if you use a custom response handler. Because of this, your event handler must handle the case where they are not set: it is safe to assume a failure in this case. -
The event handler I am sharing computes
errorTargeteverytime, which looks wasteful. This is needed because htmx can swap the whole<body>contents, leading toerrorTargetreferencing a node that has been swapped out. Becausedocument.getElementByIdis very fast (a simple table lookup), it is not worth micro-optimizing. You need to keep that pitfall in mind for the rest of your progressive enhancement code: if you really need to keep references to DOM nodes, you should register a callback with htmx.onLoad to refresh these references. -
The event handler I am sharing assumes that a pages does not send several htmx requests in parallel. If your does, you might want to use toast notifications instead, to avoid requests racing for the shared state held in the
#htmx-alertelement.
Closing words
htmx has been a pleasure to use, and I strongly recommend it for projects where a full SPA is not needed. It is rock-solid and easy to progressively add to your stack, alongside other progressive enhancements.