.end() and .addBack(): The Chaining Verbs Nobody Learned
Every jQuery traversal - .find(), .filter(), .children(), .next() - doesn't replace your selection; it pushes a new one onto an internal stack, keeping the old one underneath. Two methods work that stack, and they're the difference between chains that read like sentences and jQuery soup with four temp variables.
.end(): pop back to the previous selection
$('.order-row')
.find('.status')
.text('Shipped')
.addClass('text-green')
.end() // back to .order-row
.find('.actions button')
.prop('disabled', true)
.end() // back to .order-row again
.addClass('row-complete');
One chain, three different targets, no variables. The indentation convention (indent after a traversal, dedent at .end()) makes the stack visible - each .end() closes an indentation level like a closing brace. Without .end(), that code needs const $row = ... plus three statements; fine too, but the chain version keeps the "these operations belong together" reading intact.
.addBack(): the previous set AND the current one
// Style a fieldset's rows AND the fieldset itself
$('#shipping-fieldset')
.find('.field-row')
.addBack() // selection = rows + the fieldset
.addClass('is-disabled');
// Classic real case: operate on an element and its descendants' inputs
$form.find(':input').addBack($form.is(':input') ? undefined : null); // contrived...
// The genuinely common one - sibling ranges:
$startRow.nextUntil('.section-end').addBack().wrapAll('<tbody class="group">');
.addBack() merges the pre-traversal selection into the current one - "and also the thing I started from". The nextUntil().addBack() combo is its killer app: nextUntil excludes the starting element, addBack restores it, and now you can wrap or manipulate a contiguous range including its anchor. It even takes a selector to filter what gets added back: .addBack('.also-this').
How deep does the stack go?
$('.a').find('.b').filter('.c').end().end(); // back to $('.a') - two pops
$('.a').end(); // an EMPTY jQuery set - the stack floor
Every traversal pushes; .end() pops once; popping past the original selection yields the empty set (a quiet source of "why did my chain stop working"). Methods like .add() and .not() also push - it's uniform.
Taste limits
Past two levels of .end() nesting, future readers (you, in March) lose the plot - break into variables at that point without shame. And in modern code, the equivalent is just... multiple querySelector calls on a held reference, which is fine. But for reading the jQuery you've inherited - where these chains already exist - knowing that .end() is a stack pop turns cryptic one-liners into obviously-structured code. It's the jQuery feature that most reliably makes people say "wait, it keeps a stack?" Twenty years, still surprising people.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.