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
- 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).
- 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)).
- 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.
- 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.
- 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
- Treat HX-Retarget and responseHandling[].target as part of your API contract: document which selectors each endpoint emits and add a test asserting the matching element is always rendered on those routes.
- Prefer id selectors over class/extended selectors for retargets — ids rarely collide or go missing.
- If the target is conditional, omit the HX-Retarget header server-side when the element would be absent, rather than emitting a selector that may not match.
- Register one global htmx:targetError listener in your bootstrap file so a missed retarget is always logged (and can post to your error tracker) instead of failing silently after the throw.
- Keep responseHandling overrides minimal and code-scoped; a global {code:'4xx', target:'#x'} fires for every 4xx response and is the most common way an unrelated route trips this error.
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.