Setting a breakpoint in an XSLT stylesheet
An XSLT breakpoint does not stop a line — it stops a node being processed by a template. That difference explains most breakpoints that never fire.
In an imperative language, a breakpoint suspends a line. In XSLT it suspends the processing of one node by one template — and that difference accounts for very nearly every breakpoint that appears to be ignored.
A breakpoint set on a template that matches no node never fires. This is not a broken tool: it is the information you were looking for, arriving in a form nobody recognises.
The sequence that works has three steps:
- set the breakpoint on the
xsl:template, never on thexsl:value-ofinside it; - run, and read the current node at the moment execution stops;
- if nothing stops, do not look further inside the template body — the
matchpattern is at fault.
Why on the template, and not on the line
The instructions inside a template only run if the template has been selected. Setting the breakpoint inside it asks a question that already assumes the answer: if the template does not apply, the inner breakpoint says nothing, and its silence gets read as silence from the tool.
A breakpoint on xsl:template separates the two. It fires once per node processed. Zero
firings means zero nodes, which is a fact rather than an absence of information.
When it never fires
<xsl:template match="line[@type='discount']">
<xsl:value-of select="amount"/>
</xsl:template>
This template only fires on line elements carrying exactly type="discount". Three common
reasons why none exists:
- the predicate is narrower than the data —
type="Discount", or the attribute is missing altogether; - the document declares a namespace that the
matchpattern ignores. ⚠️ An unprefixed element name in an XPath step does not inherit the defaultxmlnsof the source document, somatch="line"never selects an element in a namespace; - no
xsl:apply-templatesreaches those nodes, and a template cannot be chosen for a node it is never offered.
⚠️ A fourth case misleads more than the other three: the template is a candidate, but another one wins.
<xsl:template match="line"><!-- yours --></xsl:template>
<xsl:template match="line"><!-- the imported one, declared later --></xsl:template>
At equal priority, the last declared wins, so an imported stylesheet can take over without anything reporting it. A breakpoint on both competing templates settles the question in a single run.
Read the current node, not the line number
When execution stops, the useful question is not where am I in the file but what am I on. The path of the current node answers both at once: it says which node is being processed, and therefore which selection path brought you there.
⚠️ This is the same reading as for variables: what you want to see is a state, not a position. A variable that looks empty at this point may hold a document node rather than the element you expect — it displays correctly and yields nothing when you navigate into it.
Conditions, and a trap that runs the other way
An XPath condition restricts a breakpoint to the passes you care about — useful on a loop of a thousand nodes where only one misbehaves.
🔴 Mind which way this fails. A condition that cannot be evaluated does not make the breakpoint silent: it makes it stop on every pass. The symptom of a faulty condition is a breakpoint that stops too often, never one that is ignored.
⚠️ This is the opposite of the intuition, and it is what costs time: you look for why "it does not stop" when the real problem is that you did not notice it stopping everywhere. Test the expression in an XPath evaluator before using it as a condition.
What this article does not cover
- the built-in rules: they are written nowhere, so no breakpoint can be set on them — their trace is text copied straight to the output;
- a breakpoint inside an XPath expression: granularity stops at the instruction;
- streaming stylesheets, where the tree is not available when execution stops;
- breakpoints on variables, which answer a different question — not when, but who writes.
Once the cause is found, the cheapest way to keep it found is a test that fails if it comes back: see XSpec in practice.
The debugging chapter of the manual covers the last two points above, along with stepping through a run once execution has stopped.