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.

Create a free account to save this page, take notes and track your progress. Sign in

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

A save button that handles slow and failed requestsTry it yourself
<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.

Was this page helpful?