Skip to main content
Most Slurp mistakes do not produce an error. A typo in a property name renders the empty string, a budget truncates the page, an unresolvable component leaves a placeholder, and the build exits 0 in all three cases. This page is organised by symptom.

Making failures visible

Three tools, in the order to reach for them.
1

Build with --verbose

Errors always print. Warnings print only with -v, and warnings are where the truncations live. A 1,500-item list quietly rendering 1,000 items looks like this and nothing else:
Without -v the same build prints Build complete. 1 page(s) emitted. and exits 0.
2

Validate with warnings on

This parses every .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.
When a value is blank and you cannot see why, put {debug expr} next to it and serve the page with slurp dev. In development mode it emits a visible <pre data-slurp-debug> holding the resolved value. In a production build it emits nothing, silently, so it is safe to leave in and useless to rely on.

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.
There are no array properties either, so 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.
The result is dead links and unstyled elements on a page that otherwise looks fine. 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.
Pass the value through the render context and read it directly instead. Detected by 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:
So a typo in an import path looks like a clean build with a missing card. Grep your output for data-slurp-component whenever a component vanishes. A layout that does not resolve has its own signature, a comment wrapper around an unfilled <slot>:
If you are embedding the compiler, the usual cause is the file registry key. The key must be exactly the path written in the 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.
The compiler says nothing. The prop arrives with the interpolation syntax intact as literal characters, so the first line above renders the text 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 undeclared slot in the same position is fine and is not an error. The renderer auto-encodes it as a self-delimiting JSON literal. Only the false declaration is refused.

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.
That renders:
The attribute is truncated and the remainder became stray attributes. Two fixes:
Detected by 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.
Better still, format the value server-side with the 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.
The per-loop cap on {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

The date 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.
There are also no time tokens. The only tokens that exist are 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.
Note what the wrong version actually produces: a space where the symbol should be, then the number. It reads as a spacing bug rather than a broken argument, and if the surrounding markup lets the minifier collapse that space it reads as nothing at all. 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" + 5 is "105". Division by zero yields 0, not an error and not Infinity.
  • Comparisons: only number-to-number and string-to-string actually compare. Every other pairing compares Equal, so > is false and >= is true.
Nothing detects this. Coerce explicitly with | 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:
Also: {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. A layout 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.