Making failures visible
Three tools, in the order to reach for them.1
Build with --verbose
-v, and warnings are
where the truncations live. A 1,500-item list quietly rendering 1,000 items
looks like this and nothing else:-v the same build prints Build complete. 1 page(s) emitted. and
exits 0.2
Validate with warnings on
.slurp file, including components and layouts, and runs
the same compile-time security walk a build runs. It catches the hard
failures (an unfiltered interpolation in a <script> body, a malformed
{match} arm) and the one advisory that matters most,
JsTemplateLiteralInAttribute.If your project has middleware, pass --middleware <dir> or every legal
request.* access reports a spurious MiddlewareScopeViolation.3
Run slurp_lint from the MCP server
The compiler will never tell you about a quoted component prop, a filter
argument that is not a literal, or
items.length. Those all compile clean
and then render wrong.slurp_lint covers exactly that set. Every rule in it describes
something both slurp validate and slurp build accept with zero output.
See Working with agents for wiring the server up. Run it
even when no agent is involved.A value renders as nothing
By far the largest category. Slurp resolves an unknown path to null and renders null as the empty string, so most mistakes end up here.Function calls
There are no callable functions in Slurp. A call parses, evaluates to null, and renders empty. No diagnostic.items.length is null for the same
reason: an identifier segment applied to an array is always null. Inside an
{each} you can read loop.count for the collection size. Outside one, pass the
count through the render context. The only call-shaped construct in the whole
language is a filter, and filters are a fixed set.
Detected by slurp_lint, rules call-always-null and array-length-property.
Loop variables in components
A component renders in its own root scope: the page globals, overlaid by the props passed explicitly. It can read a page global it was never given. What it cannot see is the caller’s local bindings, and an{each} variable is a
local binding.
loop.index, loop.first and the rest are invisible inside a component too.
Pass them as props if the component needs them.
Nothing detects this. It renders as empty values inside the component.
JavaScript template literals in attributes
Slurp interpolates${ } in attribute values itself. A backtick string written
for the browser has its slots consumed by the template engine and evaluated
against the server render context, so a client-side variable renders empty.
slurp validate reports it as a JsTemplateLiteralInAttribute
warning. It is a warning and not an error, so the build still succeeds.
Reading back a {$let} binding
{$let} seeds client state. Server-side it emits a
<script type="application/json"> payload and creates no template binding, so
reading the name back in the same template renders empty.
slurp_lint, rule let-binding-not-readable.
A misspelled path
Often the right answer.${ produts.name } renders empty exactly like every case above. Path access is
null-safe at every depth, so ${ a.b.c.d } on a null a is empty rather than an
error. Check the JSON you are feeding in before hunting for a language subtlety.
A component renders nothing, or a stray <div> appears in the HTML
The import did not resolve
A component whose import cannot be resolved does not fail the build. It renders a placeholder:data-slurp-component whenever a component vanishes.
A layout that does not resolve has its own signature, a comment wrapper around an
unfilled <slot>:
using directive or the
<layout src> attribute, so @components/Card, not components/Card.slurp. See
Embedding with Rust.
The prop was a quoted string
This is the single most common mistake in the language. Element attributes interpolate; component props do not. The same quoted text means two different things depending on whether the tag starts with an uppercase letter.Hi ${name} on
the page.
Detected by slurp_lint, rule component-prop-literal-interpolation.
A <script> tag will not compile
Unlike most things here, this one is a hard build failure.
UnsafeScriptInterpolation in a script body
A <script> body is a raw-text element: the HTML parser hands it to the
JavaScript engine without decoding character references, so the entity escaping
that protects ordinary text is inert there. Every interpolation in a script body
must therefore declare what it is.
| unsafe_js is not accepted in a script body.
A JavaScript template literal cannot be written in a script body at all, for the
same reason it cannot be written in an attribute: Slurp reads ${ as its own
interpolation. Build the string by concatenation.
UnsafeScriptInterpolation on a | js in an attribute
| js is a claim that the slot sits inside a quoted JavaScript string
literal, and a slot that declares it is emitted verbatim. Outside a string that
turns the auto-escaper off and puts nothing in its place, and escaping quote
characters cannot contain a value that lands next to a ; or a (.
An attribute is mangled and stray attributes appear
A nested double quote inside${ } ends the attribute. The lexer closes an
attribute value at its first matching quote, before interpolation is even
considered.
slurp_lint, rule unterminated-attribute-interpolation. The
compiler validates it clean.
Emitted JavaScript throws at runtime
A server decimal serialises to JSON as a string, such as"49.990000", not
as a number. In a template that is usually invisible, because filters coerce. It
matters in the JavaScript you emit, where a string has no .toFixed.
currency filter and pass
the finished string.
Nothing detects this. The compiler does not read the JavaScript you emit.
A loop stops early, or the page is cut off
Every resource budget truncates rather than failing. A page that loses the tail of a list still builds and still exits 0.- A collection is capped at 1,000 items per loop.
- A whole render is capped at 1,000,000 iterations across every loop.
- Output is capped at 16 MiB.
{each} records a warning in both build modes, so -v shows
it. {repeat n} clamps n to 1,000 with no diagnostic at all, so a quiet
build is not evidence that nothing was truncated.
See Limits for every budget, its real number, and what
crossing it does.
A date renders as a long number
Thedate filter parses ISO strings, not epoch milliseconds. It takes the text
before the first T or space and splits it on -; with fewer than three parts
the input is returned unchanged. Epoch milliseconds contain no -, so they
render verbatim.
YYYY, YY,
MMMM, MMM, MM, M, DD and D.
Nothing detects this. Return RFC 3339 dates from your server and it cannot arise.
A currency lost its symbol, or a filter did nothing
Filter arguments are read as literals, never evaluated. A variable argument is seen as the empty string, so the filter falls back to its default or emits nothing.default is the sole exception. The renderer evaluates its argument in scope, so
${ x | default(fallbackValue) } works as written.
Detected by slurp_lint, rule filter-arg-not-literal. The compiler catches it
only for limit() and filter(), where the argument is required.
If the value is genuinely dynamic, a
{match} over the currencies you support
is the workaround, or format server-side and pass a finished string.A condition takes the wrong branch
Arithmetic coerces freely. Comparisons do not.- Arithmetic: a non-numeric string, null, an array and an object all become
0, and+concatenates whenever either side is a string, so"10" + 5is"105". Division by zero yields0, not an error and notInfinity. - Comparisons: only number-to-number and string-to-string actually compare.
Every other pairing compares Equal, so
>is false and>=is true.
| int or | float whenever a
value may have arrived as a string, and remember that money always does.
A block tag is a syntax error
Two shapes that do not exist.{with} is recognised by the lexer as a block keyword but no parser supports it,
so every {with} is a hard error: Unknown block: {with}, followed by a cascade
of UnexpectedToken diagnostics from the rest of the tag. There is no
replacement. Restructure the expression, or pass a narrower value through the
context.
{match} has no {case} keyword. An arm is a bare pattern, then ->, then a
body:
{each} puts the item name first and the index second,
{each product, i in products}.
A frontmatter directive did nothing
A misspelled or non-existent frontmatter directive produces no diagnostic at all. The line is skipped. Alayout directive does not exist. Use the <layout src="..."> element in the
body.
props entries must also be one per line.
Nothing detects this. Use the MCP slurp_schema tool, or extract_schema from
the Rust or JavaScript API,
to see what the compiler actually read out of your frontmatter.
A section’s blocks vanished, but the data is still saved
This applies only when embedding Slurp and using theme blocks. Without a block catalog, a@theme-targeted block is an unknown type and is
dropped at merge time while surviving intact in storage. The page renders as
though nothing was added, and nothing is lost, so it presents as an editor that
saves but does not display.
The fix is on the host side: build a BlockCatalog from the theme’s
blocks/<name>.slurp files and thread it through, calling
merged_section_value_with or render_section_with_registry_and_blocks rather
than the catalog-free variants. See
Embedding with Rust.
Nothing detects this from a template. It is a host integration concern.
Next
Limits
Every budget with its real number, and what crossing it does.
Error codes
What each diagnostic means, including the ones the compiler never emits.
Escaping
The script and attribute escaping rules.
Filters
The full list, and the literal-argument rule in context.