Functions, modules, and error handling¶
A query grows into a program when repeated expressions become named functions,
shared code moves into library modules, and failures become part of the design.
XQuery provides all three without leaving the language: typed functions,
first-class function items, an import system, external parameters, and
try/catch.
Declare a typed function¶
A function declaration belongs in the query prolog, before the main expression:
declare function local:price($cd as element(cd)) as xs:decimal {
xs:decimal($cd/price)
};
for $cd in /catalog/cd
where local:price($cd) gt 10
return $cd/title
The parameter and return types are executable contracts. This declaration
requires exactly one cd element and promises exactly one decimal. Passing an
empty sequence, two elements, or returning a string raises a type error.
Cardinality belongs in the signature:
declare function local:total($cds as element(cd)*) as xs:decimal {
sum($cds/price ! xs:decimal(.))
};
declare function local:year($cd as element(cd)) as xs:integer? {
$cd/year ! xs:integer(.)
};
The first accepts any number of CDs; the second returns zero or one year.
Type at the boundary, infer inside
Strong parameter and return types catch bad calls early. Local let
variables rarely need declarations because the processor can infer them.
Add a local type when it expresses a real invariant, not merely to repeat
what the expression already proves.
Names and arity identify a function¶
Function identity is its expanded QName plus arity—the number of parameters. The same name may have a one-argument and a two-argument form:
declare function local:label($cd as element(cd)) as xs:string {
local:label($cd, " — ")
};
declare function local:label(
$cd as element(cd),
$separator as xs:string
) as xs:string {
concat($cd/title, $separator, $cd/artist)
};
local:label#1 and local:label#2 are different function items. XQuery does not
overload solely by parameter type: two declarations with the same name and arity
conflict even if their types differ.
Recursion replaces mutable state¶
Functions may call themselves:
declare function local:descendant-count($node as node()) as xs:integer {
count($node/*) +
sum($node/* ! local:descendant-count(.))
};
local:descendant-count(/catalog)
The more useful XML pattern is recursive construction:
declare function local:outline($element as element()) as element(item) {
<item name="{ name($element) }">{
$element/* ! local:outline(.)
}</item>
};
local:outline(/catalog)
Prefer paths, FLWOR, and folds for ordinary iteration. Reach for recursion when the data or algorithm is itself recursive: arbitrary-depth trees, graph walks, or divide-and-conquer logic.
Functions are values¶
An inline function can be stored, passed, and returned:
let $display := function($cd as element(cd)) as xs:string {
concat($cd/title, " — ", $cd/artist)
}
return /catalog/cd ! $display(.)
A named function reference uses #arity:
let $prices := /catalog/cd/price ! xs:decimal(.)
return fold-left($prices, 0, function($total, $price) {
$total + $price
})
Common higher-order functions include:
| Function | Does |
|---|---|
filter($items, $predicate) |
keeps items for which a function returns true |
for-each($items, $action) |
maps every item to a result |
fold-left($items, $zero, $combine) |
accumulates from left to right |
fold-right(...) |
accumulates from right to left |
sort($items, ..., $key) |
sorts using a key function |
map:for-each($map, $action) |
visits key/value pairs |
For example, parameterize a report by its grouping key:
declare function local:groups(
$cds as element(cd)*,
$key as function(element(cd)) as xs:anyAtomicType
) as element(group)* {
for $cd in $cds
group by $value := $key($cd)
order by $value
return <group key="{ $value }" count="{ count($cd) }"/>
};
local:groups(/catalog/cd, function($cd) { string($cd/@genre) })
An inline function is a closure: it retains variables from the scope where it was created:
let $minimum := 10
let $expensive := function($cd) {
xs:decimal($cd/price) ge $minimum
}
return filter(/catalog/cd, $expensive)/title
Dynamic lookup¶
When the function name is data, function-lookup resolves a QName and arity:
let $name := QName("http://www.w3.org/2005/xpath-functions", "upper-case")
let $function := function-lookup($name, 1)
return
if (exists($function))
then $function("XQuery")
else error(xs:QName("local:UNKNOWN-FUNCTION"))
Use dynamic lookup for plugin-like dispatch or configuration-driven pipelines. For ordinary calls, a direct function reference is clearer and gives the compiler more opportunities to check and optimize the query.
Library modules¶
A library module groups declarations under its own namespace. It has no main query expression:
xquery version "3.1";
module namespace catalog = "https://example.org/catalog";
declare function catalog:price($cd as element(cd)) as xs:decimal {
xs:decimal($cd/price)
};
declare function catalog:label($cd as element(cd)) as xs:string {
concat($cd/title, " — ", $cd/artist)
};
Import it from a main module:
xquery version "3.1";
import module namespace catalog = "https://example.org/catalog"
at "catalog-lib.xqm";
for $cd in /catalog/cd
where catalog:price($cd) gt 10
return catalog:label($cd)
The namespace URI is the module's stable identity. The at URI is a location
hint; database engines may resolve a module namespace through their own
repository instead. Keep reusable functions in library modules and environment
or request-specific wiring in the main module.
Avoid circular imports. Even where a processor diagnoses them cleanly, mutually dependent modules are a sign that shared types or helpers belong in a lower-level module.
Private functions and annotations¶
XQuery defines the annotation syntax %prefix:name; the meaning of many
annotations is processor-specific. The standard %private and %public
annotations control whether a module declaration is visible to importers:
declare %private function catalog:normalized-title(
$cd as element(cd)
) as xs:string {
normalize-space($cd/title)
};
declare %public function catalog:label($cd as element(cd)) as xs:string {
concat(catalog:normalized-title($cd), " — ", $cd/artist)
};
RESTXQ route annotations and BaseX optimization annotations use the same syntax, but are extensions. Treat them as an integration layer around portable core functions.
External variables: parameters without string substitution¶
Declare values supplied by the host application or command line as external:
declare variable $minimum as xs:decimal external;
declare variable $genre as xs:string? external := ();
for $cd in /catalog/cd
where xs:decimal($cd/price) ge $minimum
and (empty($genre) or $cd/@genre = $genre)
return $cd
The optional default after external := is used when the host supplies no value.
Binding variables keeps data separate from query source, preserves types, and
avoids the injection risk of building XQuery with string concatenation.
Library modules may expose external configuration too, but a small configuration map passed into functions is often easier to test than a large set of global variables.
Raise a deliberate error¶
error() turns a failed invariant into a named dynamic error:
declare namespace app = "https://example.org/errors";
declare function local:required-artist(
$cd as element(cd),
$artists as element(artists)
) as element(artist) {
let $matches := $artists/artist[@id = $cd/@artist-id]
return
if (count($matches) eq 1)
then $matches
else error(
QName("https://example.org/errors", "app:ARTIST-CARDINALITY"),
concat("Expected one artist for ", $cd/title),
map { "artist-id": string($cd/@artist-id), "matches": count($matches) }
)
};
The three arguments are an error QName, a human description, and an optional application value. A namespace-qualified code prevents collisions with W3C and processor error codes.
Catch dynamic errors¶
Wrap the expression that may fail:
declare namespace app = "https://example.org/errors";
try {
local:required-artist($cd, $artists)
} catch app:ARTIST-CARDINALITY {
<warning code="{ $err:code }"
message="{ $err:description }"
artist-id="{ $err:value?artist-id }"/>
} catch err:FORG0001 {
<warning message="A value could not be cast"/>
} catch * {
<warning code="{ $err:code }" message="{ $err:description }"/>
}
Catch clauses are tried in order. A QName catches one code; * catches any
remaining dynamic error. The predefined err: variables can include:
$err:codeand$err:description;$err:value, the value passed toerror;$err:module,$err:line-number, and$err:column-number;$err:additional, for processor-supplied detail.
Not every processor can provide every location field.
try cannot catch a query that does not compile
Static syntax and type errors are raised before evaluation reaches
try/catch. Catch runtime failures—bad input values, unavailable documents,
deliberate error() calls—not misspelled function names or malformed query
source.
Recover, enrich, or fail?¶
Use error handling deliberately:
- Recover when a fallback is a valid result, such as substituting cached metadata when an optional external document is unavailable.
- Enrich and rethrow when the lower-level error lacks business context.
- Fail immediately when continuing would produce a believable but incorrect report.
To rethrow with context:
try {
doc($uri)
} catch * {
error(
QName("https://example.org/errors", "app:INPUT"),
concat("Could not load catalog from ", $uri, ": ", $err:description),
$err:value
)
}
Returning <error> elements from every catch is not automatically safer. It
changes failure into ordinary data, which callers may overlook. Reserve that
pattern for APIs whose response vocabulary explicitly models errors.
Where to go next¶
Updating and indexing XML databases applies functions and error behavior to persistent data, then shows how database indexes change query performance.