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 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 (paste the selector into the Elements panel find box).
  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 that is guaranteed present.
  4. If the target is rendered conditionally, ensure it is in the DOM before the request fires (render a hidden placeholder, or issue the request only after the fragment exists), or have the server fall back to omitting HX-Retarget when the element would be absent.
  5. If using extended selector syntax, verify the keyword and the leading space are correct ('closest .container', not 'closest.container') and that elt is a descendant/sibling as the operator requires.

Example fix

// before — server sends a header for an element that may not exist
//   HX-Retarget: #user-toast
// and #user-toast is absent on this page -> throws 'Invalid re-target #user-toast'

// after — guard on the client before relying on retarget, and/or make the
// element always present:
<div id="user-toast" hidden></div>
<button hx-post="/save" hx-target="#user-toast">Save</button>

// or capture the event to recover instead of letting the throw kill the request:
document.body.addEventListener('htmx:targetError', (e) => {
  console.warn('retarget missed, falling back to elt:', 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
  // htmx extended syntax: leading keyword + space, else plain CSS
  return document.querySelector(selector) != null
}

// example gate before triggering the request
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

// Narrows a user/template-supplied string to a known-good retarget selector.
// Uses htmx's own extended-query resolver when htmx is on the page.
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)   // internal extended resolver
    : document.querySelector(selector)
  return resolved instanceof Element
}

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

Try / catch

// htmx does NOT wrap resolveRetarget in a try/catch you can hook; the supported
// recovery surface is the htmx:targetError event fired one statement before the
// throw. Listen on a stable ancestor (e.g. body) and react there.
document.body.addEventListener('htmx:targetError', (e) => {
  const { target } = e.detail            // the bad selector
  const triggeringElt = e.target          // the element that issued the request
  console.error('[htmx] invalid re-target', target, triggeringElt)
  // optional: surface user-visible feedback, or re-issue against a known fallback
})

// If you truly need a synchronous try/catch around a manual htmx.ajax call,
// wrap it — but the throw originates inside the XHR onload handler, so the
// event listener above is the only reliable recovery point:
try {
  htmx.ajax('POST', '/save', { target: '#safe', values: form })
} catch (e) {
  // catches only synchronous setup errors, NOT the async retarget throw
  console.error(e)
}

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 src/htmx.js:4854 (responseHandling.target) and 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. For the editable src/htmx.js copy, fix the source directly.

Related errors


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