bigskysoftware/htmx · error · Error

Invalid re-target ${target}

Error message

Invalid re-target ${target}

What it means

htmx retargets the swap destination via either the HX-Retarget response header or a target field on a responseHandling config entry matched by HTTP status code. resolveRetarget resolves that selector with querySelectorExt (which supports htmx's extended syntax: 'closest ', 'next ', 'previous ', 'find ', plus standard CSS), and if no element matches it fires the htmx:targetError event and throws, because htmx cannot decide where to place the response body. The throw is intentional and fatal for that request — the swap is aborted.

Source

Thrown at www/static/src/htmx.js:4795

        window.document.title = title
      }
    }
  }

  /**
   * Resove the Retarget selector and throw if not found
   * @param {Element} elt
   * @param {String} target
   * @returns {Element}
   */
  function resolveRetarget(elt, target) {
    if (target === 'this') {
      return elt
    }
    const resolvedTarget = asElement(querySelectorExt(elt, target))
    if (resolvedTarget == null) {
      triggerErrorEvent(elt, 'htmx:targetError', { target })
      throw new Error(`Invalid re-target ${target}`)
    }
    return resolvedTarget
  }

  /**
   * @param {Element} elt
   * @param {HtmxResponseInfo} responseInfo
   */
  function handleAjaxResponse(elt, responseInfo) {
    const xhr = responseInfo.xhr
    let target = responseInfo.target
    const etc = responseInfo.etc
    const responseInfoSelect = responseInfo.select

    if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return

    if (hasHeader(xhr, /HX-Trigger:/i)) {
      handleTriggerHeader(xhr, 'HX-Trigger', elt)

View on GitHub (pinned to ad56dff71e)

Solutions

  1. Open DevTools -> Network, inspect the failing XHR's Response Headers, read the exact HX-Retarget value, then confirm an element matching it exists in the DOM at response time.
  2. Add a global listener to capture the offending selector at runtime: document.body.addEventListener('htmx:targetError', e => console.error('bad retarget:', e.detail.target, e.target)).
  3. Fix the mismatch: correct the typo in the backend header or in htmx.config.responseHandling, or change the selector to one guaranteed present.
  4. If the target is rendered conditionally, ensure it is in the DOM before the request fires (render a hidden placeholder), or have the server omit HX-Retarget when the element would be absent.
  5. If you must change htmx itself, edit src/htmx.js (the source of truth) and rebuild/re-copy into www/static/src/ so the vendored copy does not drift — do not patch www/static/src/htmx.js in isolation.

Example fix

// before — server sends HX-Retarget: #user-toast but #user-toast is absent -> throws

// after — make the element always present, then retarget safely:
<div id="user-toast" hidden></div>
<button hx-post="/save" hx-target="#user-toast">Save</button>

// or recover from the throw via the event instead:
document.body.addEventListener('htmx:targetError', (e) => {
  console.warn('retarget missed, falling back:', e.detail.target)
})
Defensive patterns

Strategy: validation

Validate before calling

// Run before issuing an htmx request whose server will send HX-Retarget,
// to confirm the selector already resolves against the DOM.
function retargetSelectorExists(rootEl, selector) {
  if (selector === 'this') return true // resolveRetarget short-circuits on 'this'
  if (!selector || typeof selector !== 'string') return false
  return document.querySelector(selector) != null
}

const btn = document.querySelector('#save-btn')
const sel = '#user-toast'
if (!retargetSelectorExists(btn, sel)) {
  console.warn('HX-Retarget target missing; skipping request')
} else {
  htmx.trigger(btn, 'submit')
}

Type guard

function isResolvableRetarget(elt, selector) {
  if (typeof selector !== 'string' || selector.length === 0) return false
  if (selector === 'this') return true
  const resolved = elt instanceof Element
    ? htmx._('querySelectorExt')(elt, selector)
    : document.querySelector(selector)
  return resolved instanceof Element
}

/** @param {unknown} s */
function asRetarget(s) {
  return typeof s === 'string' && isResolvableRetarget(document.body, s) ? s : null
}

Try / catch

// Recovery surface is the htmx:targetError event (fired just before the throw);
// the throw lives in the async XHR onload, so a sync try/catch will not catch it.
document.body.addEventListener('htmx:targetError', (e) => {
  const { target } = e.detail
  console.error('[htmx] invalid re-target', target, e.target)
})

Prevention

When it happens

Trigger: Server returns an HX-Retarget response header whose CSS/extended selector matches zero elements; OR htmx.config.responseHandling contains an entry (e.g. {code:'422', target:'#form-errors'}) whose target selector is absent in the DOM when the XHR resolves. Reached via handleAjaxResponse at www/static/src/htmx.js:4854 (responseHandling.target) and www/static/src/htmx.js:4863 (HX-Retarget header). The special value 'this' short-circuits and returns elt, so it never throws.

Common situations: Typo or stale id/class in the HX-Retarget header sent by the backend; target element lives in a fragment that is conditionally rendered and is absent on the current code path; the element existed when the request was issued but was removed by a concurrent swap/removal before the response arrived; an extended selector like 'closest div' or 'next .item' is evaluated relative to elt which has no matching ancestor/sibling; selector targets an element inside a shadow root or different document. NOTE: www/static/src/htmx.js is a vendored/published copy that is byte-identical to src/htmx.js — a fix made in src/ will NOT propagate here until the static asset is rebuilt/copied; editing only this file risks drift.

Related errors


AI-assisted analysis of bigskysoftware/htmx@ad56dff71e (2026-08-13). Data as JSON: /api/errors/e890529dad4f2e5b. Report an issue: GitHub.