#Error Handling
XSLT 3.0 adds structured error handling to the language. Before 3.0, a runtime error typically terminated the transformation — your only tool was xsl:message terminate="yes" to fail intentionally. Now you can catch errors, recover gracefully, and write assertions that validate your data during transformation.
#Contents
#xsl:try / xsl:catch
xsl:try wraps code that might fail. If a dynamic error occurs inside the xsl:try body, execution jumps to the xsl:catch block instead of terminating the transformation.
#Basic Structure
<xsl:try>
<!-- code that might fail -->
<xsl:catch>
<!-- recovery code -->
</xsl:catch>
</xsl:try>#Example: Handling Type Conversion Errors
<xsl:template match="product">
<div class="product">
<h3><xsl:value-of select="name"/></h3>
<xsl:try>
<p class="price">$<xsl:value-of select="format-number(number(price), '#,##0.00')"/></p>
<xsl:catch>
<p class="price error">Price unavailable</p>
</xsl:catch>
</xsl:try>
</div>
</xsl:template>If price contains "TBD" instead of a number, the number() conversion fails. Instead of crashing the entire transformation, the catch block produces a fallback message.
C# parallel:
try
{
html += $"<p class='price'>${price:N2}</p>";
}
catch (FormatException)
{
html += "<p class='price error'>Price unavailable</p>";
}#What Can Be Caught
xsl:try/xsl:catch handles dynamic errors — errors that occur during execution:
-
Type conversion failures (
xs:integer("abc")) -
Division by zero
-
Invalid function arguments
-
Document loading errors (
doc('missing.xml')) -
xsl:message terminate="yes"(the termination becomes a catchable error in XSLT 3.0) -
Errors raised by
error()function -
Schema validation failures
It does not catch:
-
Static errors (syntax errors, undeclared variables) — these are compile-time errors
-
Type errors from
asdeclarations — these are checked beforetrycan intervene
#Return Value
xsl:try is a sequence constructor — it produces either the result of the try body (if successful) or the result of the catch body (if an error occurred). You can use it anywhere a sequence constructor is allowed, including in variable declarations:
<xsl:variable name="parsed-date" as="xs:string">
<xsl:try>
<xsl:sequence select="format-date(xs:date(@date), '[MNn] [D], [Y]')"/>
<xsl:catch>
<xsl:sequence select="string(@date)"/> <!-- fall back to raw string -->
</xsl:catch>
</xsl:try>
</xsl:variable>#Error Variables
Inside xsl:catch, several variables in the err namespace provide information about the error that occurred. You must declare the namespace to use them:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:err="http://www.w3.org/2005/xqt-errors"
version="3.0">#Available Variables
|
Variable |
Type |
Description |
|---|---|---|
|
|
|
Error code (e.g., |
|
|
|
Human-readable error message |
|
|
|
Additional error data (if provided by the error raiser) |
|
|
|
URI of the stylesheet module where the error occurred |
|
|
|
Line number where the error occurred |
|
|
|
Column number where the error occurred |
#Example: Detailed Error Logging
<xsl:template match="product" xmlns:err="http://www.w3.org/2005/xqt-errors">
<xsl:try>
<div class="product">
<h3><xsl:value-of select="name"/></h3>
<p>$<xsl:value-of select="format-number(xs:decimal(price), '#,##0.00')"/></p>
<p>Added: <xsl:value-of select="format-date(xs:date(@added), '[MNn] [D], [Y]')"/></p>
</div>
<xsl:catch>
<div class="product error">
<h3><xsl:value-of select="name"/></h3>
<p class="error-message">
Error processing this product:
<xsl:value-of select="$err:description"/>
</p>
<xsl:message>
ERROR in product <xsl:value-of select="@id"/>:
Code: <xsl:value-of select="$err:code"/>
Description: <xsl:value-of select="$err:description"/>
Module: <xsl:value-of select="$err:module"/>
Line: <xsl:value-of select="$err:line-number"/>
</xsl:message>
</div>
</xsl:catch>
</xsl:try>
</xsl:template>C# parallel: The error variables map to exception properties:
try { /* ... */ }
catch (Exception ex)
{
// $err:code -> ex.GetType().Name or ex.HResult
// $err:description -> ex.Message
// $err:value -> ex.Data
// $err:module -> ex.Source
// $err:line-number -> ex.StackTrace (parsed)
}#Selective Error Catching
The errors attribute on xsl:catch lets you specify which errors to catch. Errors not listed propagate up and terminate the transformation (or are caught by an outer xsl:try).
#Catching Specific Error Codes
<xsl:try>
<xsl:sequence select="xs:decimal(price)"/>
<xsl:catch errors="err:FORG0001">
<!-- Only catch "invalid value for cast" errors -->
<xsl:sequence select="0"/>
</xsl:catch>
</xsl:try>#Catching Multiple Error Codes
List multiple error codes separated by spaces:
<xsl:catch errors="err:FORG0001 err:FOAR0002 err:FODT0002">
<!-- Handle any of these three error types -->
</xsl:catch>#Catching All Errors in a Namespace
Use a wildcard * for the local name:
<xsl:catch errors="err:*">
<!-- Catch any standard XPath/XSLT error -->
</xsl:catch>#Common Error Codes
|
Code |
Description |
|---|---|
|
|
Invalid value for cast/constructor |
|
|
Division by zero |
|
|
Error loading document (doc() / document()) |
|
|
Overflow/underflow in date/time operation |
|
|
Argument to fn:data() is a function item |
|
|
Type error (value does not match expected type) |
|
|
xsl:message with terminate="yes" |
#Nested Try/Catch
You can nest xsl:try blocks for layered error handling:
<xsl:try>
<!-- Outer try: catches document loading errors -->
<xsl:variable name="lookup" select="doc('lookup.xml')"/>
<xsl:for-each select="//product">
<xsl:try>
<!-- Inner try: catches per-product errors -->
<div class="product">
<xsl:apply-templates select="."/>
</div>
<xsl:catch>
<div class="product error">
<p>Error processing <xsl:value-of select="name"/>: <xsl:value-of select="$err:description"/></p>
</div>
</xsl:catch>
</xsl:try>
</xsl:for-each>
<xsl:catch errors="err:FODC0002">
<p class="system-error">Lookup table could not be loaded. Please try again later.</p>
</xsl:catch>
</xsl:try>C# parallel: Nested try/catch with specific exception types:
try
{
var lookup = LoadDocument("lookup.xml");
foreach (var product in products)
{
try
{
RenderProduct(product, lookup);
}
catch (FormatException ex)
{
RenderErrorCard(product, ex.Message);
}
}
}
catch (FileNotFoundException)
{
RenderSystemError("Lookup table could not be loaded.");
}#xsl:assert
xsl:assert tests a condition and raises an error if the condition is false. It is a declarative way to validate assumptions about your data during transformation.
#Basic Usage
<xsl:template match="product">
<xsl:assert test="@id" error-code="Q{http://example.com/err}MISSING-ID">
Product element must have an @id attribute.
</xsl:assert>
<xsl:assert test="price castable as xs:decimal" error-code="Q{http://example.com/err}BAD-PRICE">
Product <xsl:value-of select="@id"/>: price must be a valid decimal number,
got '<xsl:value-of select="price"/>'.
</xsl:assert>
<!-- If we get here, both assertions passed -->
<div class="product" data-id="{@id}">
<xsl:apply-templates/>
</div>
</xsl:template>#Attributes
|
Attribute |
Description |
|---|---|
|
|
XPath expression that must evaluate to true. If false, the assertion fails and an error is raised. |
|
|
Optional QName for the error code (catchable by |
|
|
Optional expression whose value is attached to the error as |
#The select Attribute
The select attribute provides structured data about the assertion failure, accessible as $err:value in a catch block:
<xsl:assert test="price > 0"
error-code="Q{http://example.com/err}INVALID-PRICE"
select="map { 'product-id': string(@id), 'price': string(price) }">
Price must be positive.
</xsl:assert>In a catch block:
<xsl:catch errors="Q{http://example.com/err}INVALID-PRICE">
<xsl:variable name="info" select="$err:value"/>
<p>Product <xsl:value-of select="$info?product-id"/> has invalid price:
<xsl:value-of select="$info?price"/></p>
</xsl:catch>#Use in Validation Scenarios
xsl:assert is powerful for building validation into your transformation:
<xsl:template match="order">
<!-- Validate the order before processing -->
<xsl:assert test="line-item">
Order <xsl:value-of select="@id"/> has no line items.
</xsl:assert>
<xsl:assert test="every $li in line-item satisfies $li/@quantity > 0">
Order <xsl:value-of select="@id"/> contains line items with invalid quantities.
</xsl:assert>
<xsl:assert test="customer-ref and key('customers-by-id', customer-ref)">
Order <xsl:value-of select="@id"/> references unknown customer
'<xsl:value-of select="customer-ref"/>'.
</xsl:assert>
<!-- Process the validated order -->
<xsl:apply-templates/>
</xsl:template>
C# parallel: Debug.Assert() or guard clauses:
Debug.Assert(order.LineItems.Any(), $"Order {order.Id} has no line items.");
// Or with guard clauses:
if (!order.LineItems.Any())
throw new ValidationException($"Order {order.Id} has no line items.");The key difference: xsl:assert errors can be caught by xsl:try/xsl:catch, making them recoverable. Debug.Assert in C# is typically only active in debug builds.
#xsl:fallback
xsl:fallback provides a graceful degradation mechanism when a stylesheet uses instructions that the processor does not support. This is primarily useful for forwards compatibility — writing stylesheets that use newer features while still working on older processors.
#Basic Usage
<xsl:some-future-instruction>
<!-- This instruction might not exist in the current processor -->
<xsl:fallback>
<!-- This runs if the processor does not recognize the parent instruction -->
<xsl:message>Note: some-future-instruction is not supported. Using fallback.</xsl:message>
<!-- Alternative implementation -->
</xsl:fallback>
</xsl:some-future-instruction>#Forwards Compatibility Example
Suppose you want to use a hypothetical XSLT 4.0 instruction but need the stylesheet to work on XSLT 3.0 processors:
<xsl:stylesheet version="4.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="product">
<!-- Try to use a 4.0 feature -->
<xsl:switch select="@status">
<xsl:when test="'active'">
<span class="badge-green">Active</span>
</xsl:when>
<xsl:when test="'discontinued'">
<span class="badge-red">Discontinued</span>
</xsl:when>
<xsl:fallback>
<!-- If xsl:switch is not supported, fall back to xsl:choose -->
<xsl:choose>
<xsl:when test="@status = 'active'">
<span class="badge-green">Active</span>
</xsl:when>
<xsl:when test="@status = 'discontinued'">
<span class="badge-red">Discontinued</span>
</xsl:when>
</xsl:choose>
</xsl:fallback>
</xsl:switch>
</xsl:template>
</xsl:stylesheet>When a 4.0 processor runs this, it executes xsl:switch normally. When a 3.0 processor encounters the unrecognized xsl:switch, it sees the stylesheet declares version="4.0" (a forwards-compatible version), finds the xsl:fallback child, and executes that instead.
#How Forwards Compatibility Works
-
If the stylesheet declares a version higher than the processor supports (e.g.,
version="4.0"on a 3.0 processor), the processor enters forwards-compatible mode -
In this mode, unrecognized instructions are not immediately an error — the processor looks for
xsl:fallbackchildren -
If
xsl:fallbackis found, it is executed in place of the unrecognized instruction -
If no
xsl:fallbackis found, a runtime error is raised (which can be caught byxsl:try/xsl:catch)
#Multiple Fallbacks
You can provide multiple xsl:fallback elements — all of them execute if the parent is unsupported:
<xsl:hypothetical-instruction>
<xsl:fallback>
<xsl:message>Using fallback for hypothetical-instruction</xsl:message>
</xsl:fallback>
<xsl:fallback>
<!-- Alternative implementation -->
<xsl:apply-templates/>
</xsl:fallback>
</xsl:hypothetical-instruction>
C# parallel: xsl:fallback is conceptually similar to #if preprocessor directives or feature detection:
#if NET8_0_OR_GREATER
// Use new API
var result = SomeNewApi();
#else
// Fallback for older frameworks
var result = LegacyApproach();
#endif#xsl:message with terminate
Before XSLT 3.0's xsl:try/xsl:catch, the only way to signal an error was xsl:message terminate="yes". This is still useful and widely used:
#As a Guard Clause
<xsl:template match="product">
<xsl:if test="not(@id)">
<xsl:message terminate="yes">
FATAL: Product element at position <xsl:value-of select="position()"/>
is missing required @id attribute.
</xsl:message>
</xsl:if>
<!-- Normal processing continues if the check passed -->
<div class="product" data-id="{@id}">
<xsl:apply-templates/>
</div>
</xsl:template>#With Error Codes (XSLT 3.0)
In XSLT 3.0, xsl:message terminate="yes" raises a catchable error. You can assign an error code:
<xsl:message terminate="yes"
error-code="Q{http://example.com/err}MISSING-REQUIRED-FIELD">
Required field '<xsl:value-of select="$field-name"/>' is missing in
<xsl:value-of select="local-name()"/> element.
</xsl:message>This error can be caught:
<xsl:try>
<xsl:apply-templates select="//product"/>
<xsl:catch errors="Q{http://example.com/err}MISSING-REQUIRED-FIELD">
<p class="error">Some products have missing data. <xsl:value-of select="$err:description"/></p>
</xsl:catch>
</xsl:try>#terminate="yes" vs. xsl:assert
|
|
|
|
|---|---|---|
|
Syntax |
Requires wrapping in |
Has a built-in |
|
Message |
Content of the message element |
Content of the assert element |
|
Error code |
Via |
Via |
|
Error value |
Not directly supported |
Via |
|
Readability |
Two instructions ( |
Single instruction |
|
Pre-3.0 |
Available since XSLT 1.0 |
XSLT 3.0 only |
For validation checks, prefer xsl:assert — it is more concise and communicates intent more clearly. Use xsl:message terminate="yes" when you need the message to carry complex content or when targeting XSLT 1.0/2.0 compatibility.
#Error Recovery Patterns
#Fail-Safe Processing
Process each item independently, catching errors per item so one bad record does not stop the entire transformation:
<xsl:template match="catalog">
<html>
<body>
<h1>Product Catalog</h1>
<xsl:for-each select="product">
<xsl:try>
<xsl:apply-templates select="."/>
<xsl:catch>
<div class="product error-card">
<h3><xsl:value-of select="(name, @id, 'Unknown')[1]"/></h3>
<p class="error">This product could not be displayed:
<xsl:value-of select="$err:description"/></p>
</div>
</xsl:catch>
</xsl:try>
</xsl:for-each>
</body>
</html>
</xsl:template>C# parallel: This is the "continue on error" pattern:
foreach (var product in products)
{
try
{
RenderProduct(product);
}
catch (Exception ex)
{
RenderErrorCard(product, ex.Message);
}
}#Safe Document Loading
Gracefully handle missing or invalid external documents:
<xsl:function name="my:safe-doc" as="document-node()?">
<xsl:param name="uri" as="xs:string"/>
<xsl:try>
<xsl:sequence select="doc($uri)"/>
<xsl:catch errors="err:FODC0002">
<xsl:message>WARNING: Could not load document '<xsl:value-of select="$uri"/>'</xsl:message>
<xsl:sequence select="()"/> <!-- return empty sequence -->
</xsl:catch>
</xsl:try>
</xsl:function>
<!-- Usage -->
<xsl:variable name="lookup" select="my:safe-doc('lookup-table.xml')"/>
<xsl:if test="$lookup">
<!-- Document loaded successfully, use it -->
</xsl:if>#Validation with Error Collection
Validate all records and collect errors before deciding how to proceed:
<xsl:template match="catalog">
<!-- Collect validation errors -->
<xsl:variable name="errors" as="element(error)*">
<xsl:for-each select="product">
<xsl:if test="not(@id)">
<error product="{position()}" field="id">Missing @id attribute</error>
</xsl:if>
<xsl:if test="not(price castable as xs:decimal)">
<error product="{@id}" field="price">
Invalid price: '<xsl:value-of select="price"/>'
</error>
</xsl:if>
<xsl:if test="not(name)">
<error product="{@id}" field="name">Missing name element</error>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:choose>
<xsl:when test="$errors">
<!-- Show validation report -->
<html>
<body>
<h1>Validation Errors</h1>
<p><xsl:value-of select="count($errors)"/> error(s) found.</p>
<table>
<thead>
<tr><th>Product</th><th>Field</th><th>Error</th></tr>
</thead>
<tbody>
<xsl:for-each select="$errors">
<tr>
<td><xsl:value-of select="@product"/></td>
<td><xsl:value-of select="@field"/></td>
<td><xsl:value-of select="."/></td>
</tr>
</xsl:for-each>
</tbody>
</table>
</body>
</html>
</xsl:when>
<xsl:otherwise>
<!-- All valid — proceed with normal output -->
<xsl:apply-templates select="product"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>#Default Values with Try/Catch
Use try/catch to provide default values when data is malformed:
<xsl:function name="my:safe-decimal" as="xs:decimal">
<xsl:param name="value" as="xs:string"/>
<xsl:param name="default" as="xs:decimal"/>
<xsl:try>
<xsl:sequence select="xs:decimal($value)"/>
<xsl:catch>
<xsl:sequence select="$default"/>
</xsl:catch>
</xsl:try>
</xsl:function>
<xsl:function name="my:safe-date" as="xs:string">
<xsl:param name="value" as="xs:string"/>
<xsl:try>
<xsl:sequence select="format-date(xs:date($value), '[MNn] [D], [Y]')"/>
<xsl:catch>
<xsl:sequence select="$value"/> <!-- return raw string if unparseable -->
</xsl:catch>
</xsl:try>
</xsl:function>
<!-- Usage -->
<xsl:variable name="price" select="my:safe-decimal(price, 0)"/>
<xsl:variable name="display-date" select="my:safe-date(@date)"/>
C# parallel: TryParse pattern:
static decimal SafeDecimal(string value, decimal defaultValue) =>
decimal.TryParse(value, out var result) ? result : defaultValue;#Assertion + Try/Catch for Controlled Validation
Combine assertions with try/catch for a "validate, then process" pattern:
<xsl:template match="order">
<xsl:try>
<!-- Assertions act as preconditions -->
<xsl:assert test="@id">Order must have an ID.</xsl:assert>
<xsl:assert test="line-item">Order must have at least one line item.</xsl:assert>
<xsl:assert test="every $li in line-item satisfies
key('products-by-id', $li/@product-ref)">
All product references must be valid.
</xsl:assert>
<!-- If all assertions pass, process normally -->
<div class="order">
<xsl:apply-templates/>
</div>
<xsl:catch>
<div class="order rejected">
<p>Order <xsl:value-of select="@id"/> rejected: <xsl:value-of select="$err:description"/></p>
</div>
</xsl:catch>
</xsl:try>
</xsl:template>C# parallel summary:
|
XSLT |
C# |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Nested |
Nested |