htmx: Build Interactive Web Apps with HTML · Production Concerns
Loading States, Races and Failures
Make a save button behave well on a slow or broken network: show progress, prevent double submits, time out, and tell the user when something failed.
What you'll build
A Save draft button that shows a spinner, can't be pressed twice, gives up after a timeout, and shows a clear message when the server returns an error or the network is down, instead of silently doing nothing.
The idea
By default, when a request fails htmx 2 changes nothing on the page. Users can't tell "saved" from "lost". Handle the three failure kinds explicitly:
| Event | Happens when |
|---|---|
htmx:responseError |
the server answered with 4xx/5xx |
htmx:sendError |
the request couldn't be sent (offline, DNS, CORS) |
htmx:timeout |
no answer within hx-request='{"timeout": ...}' or htmx.config.timeout |
htmx:afterRequest fires in all cases with event.detail.successful, so one handler can report any failure.
For races, hx-sync decides what happens when a request starts while another is running: drop (ignore the new
one, the default), abort, replace (cancel the old one), or queue. hx-sync="closest form:abort" coordinates
all elements of a form.
The code
<form hx-post="/drafts" hx-target="#saved" hx-disabled-elt="find button" hx-request='{"timeout": 2500}'
hx-on::after-request="document.getElementById('error').textContent = event.detail.successful ? '' :
(event.detail.xhr.status ? 'The server had a problem (' + event.detail.xhr.status + '). Your text is still here; try again.'
: 'No answer from the server. Check your connection and try again.')">
<textarea name="text" rows="2" cols="40">My draft</textarea><br>
<label><input type="radio" name="mode" value="ok" checked> Normal</label>
<label><input type="radio" name="mode" value="slow"> Very slow</label>
<label><input type="radio" name="mode" value="error"> Server error</label><br>
<button>Save draft</button> <span class="htmx-indicator">Saving...</span>
</form>
<p id="saved"></p>
<p id="error" role="alert" style="color:#b91c1c"></p>
<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
server.post("/drafts", (req) => {
if (req.body.mode === "error") return { status: 500, body: "Internal Server Error" };
if (req.body.mode === "slow") // answers after 4 s, longer than the form's 2.5 s timeout
return new Promise((resolve) => setTimeout(() => resolve("Saved, but very late."), 4000));
return `Draft saved at ${new Date().toLocaleTimeString()}.`;
}, { delay: 600 });
</script>
For site-wide handling, listen once on document.body for htmx:responseError and htmx:sendError and show a
toast, instead of repeating hx-on everywhere.
Common mistakes
- No error handling: a failed save looks like nothing happened, and users lose work.
- No timeout: in htmx 2 the default is no timeout, so a hung request keeps the spinner forever.
- Re-enabling the form before the request finishes, or not disabling it at all: double submits create duplicates. Make important POST endpoints idempotent too (for example with a unique key per form).
- Swapping server error pages (stack traces) into the page: keep error responses short and safe.
Key points
- Show progress (
htmx-indicator), prevent repeats (hx-disabled-elt,hx-sync), and set a timeout. - Report failures with
htmx:afterRequest/htmx:responseError/htmx:sendError. - Keep the user's input on failure so they can retry.
Further reading: htmx events · hx-sync · hx-request
Practice questions
What does htmx 2 show the user by default when a request returns 500?
Answer
Nothing: 5xx responses aren't swapped. You must handle htmx:responseError (or htmx:afterRequest) to tell the user.
Which event tells you the network failed, as opposed to the server returning an error?
Answer
htmx:sendError (and htmx:timeout for timeouts); htmx:responseError is for HTTP error statuses.
What is htmx 2's default request timeout, and how do you set one?
Answer
0, meaning no timeout. Set htmx.config.timeout globally or hx-request='{"timeout": 5000}' per element.
Open in the app to mark lessons complete, take notes and track your progress.