Skip to content

Error Handling

Introduction · Feature index · Cheatsheet

Rea has two audiences and they never share a pipe.

The reader gets prose. Every failure has a defined, silent fallback, and no error text ever reaches the page — not a message, not a placeholder token, not a bare identifier. This is a language guarantee, not a runtime detail.

The author gets records: structured, code-identified, position-carrying data with no rendered form. A record is never shown to a reader at any severity. reast validate prints them; the editor underlines them; a host formats them from code + args + locale.

The two channels are the whole design. A failure produces a fallback and a record, and neither one substitutes for the other.

Severities

Every code carries exactly one severity, fixed in the engine's registry. A call site never chooses one, so two places noticing the same condition cannot disagree about how bad it is.

SeverityWhat it meansFails CI
fatalThe artefact cannot be loaded at all. Package and extension faults only.yes
errorAn authoring mistake with a reader-visible consequence: content is lost, dead or wrong.yes
warningAn authoring mistake with no reader-visible consequence yet.under --strict
degradedCorrect behaviour under a reduced environment or conformance level.never
infoHygiene, style and authoring notes.no

degraded is never promoted, not even by --strict. Promoting it would defeat the reason it is a separate severity: an author has to be able to tell "my Platform feature did nothing here, and that is by design" from "I made a mistake".

Nothing in parse/ is fatal. Any UTF-8 text is a valid Rea document — a .rea file never fails to parse.

Code partitions

A code is a lowercase, slash-partitioned string; the prefix is the range, so codes sort, grep and glob.

PartitionRaised by
pkg/Archive, manifest, integrity, decryption
ext/.rext load-time trust and grammar, {use} resolution
parse/Reading one file
link/Resolving names across the whole package
eval/Evaluating an expression
flow/Running the story: limits, control flow, saves
env/The environment the story is read in: media, sensors, readers
style/Hygiene and authoring notes
meta/The record stream itself

The escape rule

A gate that depends on state neither the author nor the reader controls needs a way out, and that rule belongs to every waiting condition rather than to one block. A {wait} or {waypoint} whose expression reads context.* and declares neither escape= nor escape_to= gets link/wait-no-escape or link/waypoint-no-escape — a warning, not an error, because a deliberate hard physical gate with no digital bypass is a legitimate design.

Its now-mode counterpart is link/context-no-fallback: an {if} on a real-world source with no {else} renders nothing at all when the source is denied or has not delivered yet, which is a blank page where the author expected one of two scenes. And a condition reading a context. subtree no platform provides gets link/unknown-context-source — it could never become true, so the story would stop there for good.

What the reader gets

stablesince 1.0automatic

A reader never sees an error: an unknown command is skipped whole, missing media falls back, an unclosed block auto-closes. The author hears about every one of them on a separate channel of coded records. This is a language guarantee, not a runtime detail, so it is frozen.

Every code has a defined fallback, and the whole table is normative: an implementation conforms by producing the stated reader experience for each one. It is generated from the engine's code registry by scripts/check-spec-fallback-table.mjs, so a new code cannot ship without stating what the reader gets, and the spec cannot drift from the implementation. Do not edit the block by hand.

An unknown command is skipped whole — including its block, if it opens one. It is not printed as an expression. Printing it put the author's markup on the reader's page, which is exactly the thing the reader channel exists to prevent.

Division by zero yields nothing, which renders as nothing. It used to yield 0, a value the reader could not tell from a real result.

pkg/ — Package, manifest, integrity (17)

CodeSeverityWhat the reader gets
pkg/invalid-archivefatalload refused
pkg/size-exceededfatalload refused
pkg/too-many-entriesfatalload refused
pkg/path-traversalfatalload refused
pkg/loader-invalid-urlfatalload refused
pkg/loader-fetch-failedfatalload refused
pkg/manifest-missingfatalload refused
pkg/manifest-invalid-jsonfatalload refused
pkg/manifest-schema-errorfatalload refused
pkg/manifest-unsupported-versionfatalload refused
pkg/manifest-older-minordegradedunknown features skipped
pkg/manifest-unknown-keyinfonone
pkg/integrity-mismatchfatalload refused
pkg/signature-mismatcherrorloads; host chrome flags source
pkg/decrypt-no-keyfatalload refused
pkg/decrypt-failedfatalload refused
pkg/session-config-invaliderrorfile ignored; no vars injected

ext/ — Extension load and {use} resolution (10)

CodeSeverityWhat the reader gets
ext/invalidfatalload refused
ext/prose-nodefatalload refused
ext/encryptedfatalload refused
ext/use-cyclefatalload refused
ext/duplicate-exportfatalload refused
ext/unresolved-usefatalload refused
ext/declared-not-packagedfatalload refused
ext/reserved-namespacefatalload refused
ext/requires-missingfatalload refused
ext/unbound-aliaserrorcall yields undefined → empty

parse/ — Reading one file (50)

CodeSeverityWhat the reader gets
parse/unterminated-blockwarningauto-closed at EOF
parse/implicitly-closed-blockinfoclosed by the same-kind opener
parse/orphan-block-endwarning{end x} dropped
parse/unterminated-code-blockwarningauto-closed at EOF
parse/unterminated-commandwarningliteral text
parse/unterminated-bracketwarningliteral text
parse/unknown-commanderrorblock skipped entirely (D9)
parse/unknown-namespaceerrorblock skipped entirely
parse/malformed-headingwarningliteral text
parse/malformed-mediawarningembed skipped
parse/malformed-blockquotewarningliteral text
parse/malformed-dialoguewarningliteral text
parse/malformed-pinwarningpin skipped
parse/malformed-boundswarningthe map draws unanchored
parse/invalid-mediasessionwarningdirective skipped
parse/invalid-vibratewarningdirective skipped
parse/empty-menuwarningmenu not shown
parse/empty-choice-groupwarninggroup not shown
parse/choice-options-cappederroroptions past the cap are not offered
parse/single-option-choice-groupinfothe one option is shown
parse/unknown-attributewarningthe attribute is ignored
parse/body-propertyerrorthe line renders as prose; the setting is lost
parse/unexpected-whenwarningthe condition is ignored
parse/missing-event-subjecterrorthe handler never runs
parse/ambiguous-event-subjectwarningonly the first subject is used
parse/unknown-eventwarningthe handler never runs
parse/missing-deck-subjecterrorthe command does nothing
parse/face-outside-cardwarningthe face text is dropped
parse/invalid-face-positionwarningthe face sits in the default band
parse/detail-outside-cardwarningthe detail is dropped
parse/layer-outside-cardwarningthe layer is dropped
parse/layer-without-imagewarningthe layer is dropped
parse/manifest-not-firstwarningthe metadata is ignored
parse/duplicate-embedded-fileerrorthe first declaration is used
parse/invalid-storylet-matcherrorstorylet never eligible
parse/inline-depth-exceededwarningdeeper inlines flatten to text
parse/document-truncatederrorcontent after the limit is lost
parse/unknown-formattinginfoliteral text
parse/formatting-not-adjacentinfoliteral text
parse/alignment-missing-spaceinfoliteral text
parse/hint-contains-markerwarningmarker treated as literal in the hint
parse/footnote-contains-bracketwarningfootnote ends at the first ]
parse/heading-depth-clampeddegradedrendered at the deepest supported
parse/indent-depth-clampeddegradedrendered at the deepest supported
parse/duplicate-anchorerrorfirst definition wins; the rest unreachable
parse/content-after-gatewarningunreachable — a gate is terminal
parse/reserved-word-misuseerrorblock skipped entirely
parse/dotless-seterrorblock skipped entirely
parse/comma-on-no-attribute-commanderrorblock skipped entirely
parse/bare-word-attribute-valueerrorblock skipped entirely
CodeSeverityWhat the reader gets
link/undefined-anchorerrordivert ignored; reading continues
link/undefined-tunnel-targeterrortunnel ignored; reading continues
link/tunnel-return-without-entryerrorreturn ignored
link/unreachable-after-divertwarningnone
link/unused-anchorinfonone
link/dead-end-anchorinforeading stops there
link/cyclic-divertinfonone
link/cyclic-variablewarningundefined → empty
link/variable-never-setwarningundefined → empty
link/undefined-functionerrorcall yields undefined → empty
link/builtin-arityerrorcall yields undefined → empty
link/redefines-builtinerrorthe built-in runs; the declaration is ignored
link/story-scope-dotless-setwarningvariable is heading-scoped, not story-scoped
link/missing-mediawarningplaceholder / skip / poster per §27
link/media-missing-altwarningimage renders without alt
link/missing-parterrorlink inert
link/missing-gate-targeterrorgate is terminal — the story ends
link/missing-part-anchorerrorpart opens at its start
link/preload-target-missingwarningnothing preloaded
link/sensor-not-declarederrorhas() is false
link/sensor-declared-unusedinfoa permission is requested for nothing
link/unreachable-choicewarningauthored content the reader cannot reach
link/undefined-replace-targeterror{replace} is a no-op
link/unknown-cardwarning{play} is a no-op (specified)
link/unknown-deckwarningthe command deals nothing
link/undealt-deckinfonone — the deck is never dealt
link/unreachable-cardwarningauthored content the reader cannot reach
link/deck-short-of-dealwarninga smaller hand than the deck asks for
link/group-deck-without-readerswarningthe deck degrades to reader scope
link/undefined-iteminfoitem stacks without a definition
link/unknown-machine-eventerror{trigger} is a no-op
link/undefined-initial-stateerrormachine has no valid state
link/undefined-state-targeterrortransition does not fire
link/storylet-requires-unset-variablewarningstorylet never eligible
link/unknown-storylet-triggerwarningstorylet never triggered
link/undefined-timer-targeterroron_expire fires into nothing
link/unknown-checkpointerror{restore} is a no-op
link/unknown-domainerrorblock skipped entirely
link/unwritten-variableerrorundefined → empty
link/possibly-unwritten-variablewarningundefined → empty
link/waypoint-no-escapewarningnone — gate behaves as authored
link/wait-no-escapewarningnone — the wait behaves as authored
link/unknown-context-sourcewarningreads unknown; an until keeps waiting
link/context-no-fallbackwarningblock skipped when the source is unavailable
link/unknown-route-waypointerrorthe stage is skipped; the route can never complete

eval/ — Evaluating an expression (33)

CodeSeverityWhat the reader gets
eval/undefined-variablewarningempty string (X-01)
eval/non-numeric-arithmeticwarningundefined → empty
eval/division-by-zerowarningundefined → empty (X-02)
eval/number-conversion-failedwarningundefined → empty
eval/conversion-failedwarningundefined → empty
eval/type-mismatch-comparisoninfocoerced per §12
eval/empty-string-false-equalityinfo"" equals false per §12
eval/matches-non-regexwarningfalse
eval/in-non-arraywarningfalse
eval/index-out-of-boundswarningundefined → empty
eval/property-on-non-objectwarningundefined → empty
eval/depth-exceedederrorundefined → empty
eval/ops-exceedederrorundefined → empty
eval/expression-too-longerrorundefined → empty
eval/invalid-expressionerrorundefined → empty
eval/string-truncatederrorreader sees a cut string
eval/variable-limit-reachederrorthe write is dropped
eval/unsafe-variable-nameerrorthe write is dropped
eval/coordinate-out-of-rangeerrorundefined → empty
eval/invalid-datetimewarningthe empty string, per §30
eval/invalid-durationwarning0 per §30
eval/format-fallbackdegradedplain string form
eval/plural-missing-othererrorundefined → empty
eval/select-no-matcherrorundefined → empty
eval/calendar-incompleteerrorundefined → empty
eval/ordinal-unsupported-localedegradednumber, no suffix
eval/invalid-dice-notationwarningundefined → empty
eval/strict-undeclared-writewarningthe write succeeds
eval/context-write-refusederrorthe write is dropped
eval/story-meta-write-refusederrorthe write is dropped
eval/card-write-refusederrorthe write is dropped
eval/unknown-cardwarningundefined → empty
eval/unknown-card-propertywarningundefined → empty

flow/ — Running the story (19)

CodeSeverityWhat the reader gets
flow/tunnel-overflowerrortunnel unwound
flow/call-depth-exceedederrorcall yields undefined
flow/config-value-invalidwarningdefault limit used
flow/iteration-limiterrorloop cut short; the scene changes
flow/node-limiterrorremaining content is lost
flow/timeouterrorremaining content is lost
flow/for-non-arraywarningloop body never runs
flow/break-outside-looperrorignored
flow/return-outside-functionerrorignored
flow/missing-argumentwarningparameter is undefined
flow/fallback-choice-takeninfofallback auto-selected (specified)
flow/no-eligible-choiceerrordead end — no choice, no fallback
flow/menu-underfilleddegradedfewer than select=N shown (specified)
flow/empty-cyclewarningnothing shown
flow/insufficient-fundsinfo{spend} refuses, nothing changes
flow/timer-replacedwarningouter on_expire never fires
flow/save-position-lostdegradednearest checkpoint / chapter start
flow/save-major-mismatcherrorhost notice + fresh start (the one specified reader-facing message)
flow/restore-condition-failedinforestore refused; reading continues

env/ — The environment the story is read in (27)

CodeSeverityWhat the reader gets
env/missing-imagedegradedplaceholder with alt text
env/missing-audiodegradedsilently skipped
env/missing-videodegradedposter frame, else placeholder
env/media-fallback-failedwarningplatform default
env/tts-unavailabledegradedsilently skipped
env/sensor-unavailabledegradedhas() is false
env/permission-denieddegradedstory degrades per §21
env/permission-revokeddegradedtreated as newly unavailable
env/requirement-unsatisfiableerrorhost refuses to start; not story text
env/request-failedwarningundefined; story continues
env/url-alias-undeclarederrorrequest fails → undefined
env/part-load-failederrorpart unreachable
env/input-coercedwarningnon-numeric input becomes 0
env/input-clampedwarningvalue clamped to the nearest bound
env/no-matchinfogentle non-match feedback (specified)
env/reader-timeoutdegradedexcluded after the 30s grace
env/vote-reader-droppeddegradedvote excluded; cast votes stand
env/vote-no-votesdegradedvote.result is undefined
env/vote-tieinforandom among tied, from the seeded stream
env/race-no-winnerdegradedrace.winner undefined; block skipped
env/lock-released-on-disconnectdegradedreleased after grace
env/role-vacatedwarningrole not reassigned; content may be unreachable
env/shared-write-conflictinfolast-write-wins
env/solo-degradationdegradedthe §20 degradation table
env/no-transportdegradedrenders single-reader
env/undo-chapter-boundarydegradedundo stops at the chapter start
env/undo-blockeddegradedlock not released; vote not retracted

style/ — Authoring hygiene (11)

CodeSeverityWhat the reader gets
style/empty-sectioninfonothing — a hygiene note has no reader consequence
style/unused-variableinfonothing — a hygiene note has no reader consequence
style/deep-nestinginfonothing — a hygiene note has no reader consequence
style/dead-conditioninfonothing — a hygiene note has no reader consequence
style/empty-anchorinfonothing — a hygiene note has no reader consequence
style/choice-without-consequenceinfonothing — a hygiene note has no reader consequence
style/choice-label-too-shortinfonothing — a hygiene note has no reader consequence
style/heading-level-skippedinfonothing — a hygiene note has no reader consequence
style/paragraph-too-longinfonothing — a hygiene note has no reader consequence
style/todoinfonothing — a hygiene note has no reader consequence
style/confusable-identifierinfonothing — a hygiene note has no reader consequence

meta/ — The record stream itself (3)

CodeSeverityWhat the reader gets
meta/budget-exceededwarningnothing — the record stream was truncated, not the story
meta/above-conformance-leveldegradedthe feature is skipped
meta/redactedwarningnothing — a record was suppressed, not content

What a record may carry

A record may name an identifier the author wrote, quote what the author literally typed, and describe the type of a runtime value. It may never carry a runtime value.

That rule is enforced by the shape of the API, not by review: there is no constructor that accepts a caller-supplied string. Quoted source is read back out of the file at a position. So a failed {set story.gold = "abc"} may report "abc", because the author typed it into the file, while the same failure on a value that arrived through {input} can only report a type name.

This binds the free-text and audio privacy guarantees of Section 19 and Section 21 to diagnostic records too, not only to story state. A {listen} that fails to match records that it failed to match — never what was said.

Reading the records

bash
reast validate                 # every .rea and .rext under data/seed
reast validate path/ --json    # the record stream, for CI
reast validate path/ --strict  # warnings fail the build too
text
story/0001.rea:124:1 error link/undefined-anchor Divert to "the_vault" — no such anchor

The exit code is non-zero on any fatal or error, in every output mode.

Rea does not have try/catch. All error handling is implicit — the runtime recovers, the reader's experience is never interrupted, and the author reads the record.

Fallback values

draft[!map < media/map.png, fallback="media/low.png"]

A second source tried before the platform’s own placeholder. Specified; the attribute is currently parsed and ignored.

Where it makes sense, syntax supports optional inline fallback values:

rea
[!map < media/map.png, fallback="media/map-lowres.png"]
[?thunder < sounds/thunder.mp3, fallback="sounds/rain.mp3"]

If the primary resource fails, the fallback is used. If the fallback also fails, the platform applies its default graceful behavior (placeholder for images, silence for audio, etc.).

External API access

experimentalsince 1.0manifest `allowed_urls`, referenced by alias

URLs live in the manifest and never in .rea text, so every external call is declared and auditable; the alias call surface is still settling.

External API calls (network requests from within a story) must be declared in manifest.json via allowed_urls. URLs must not appear anywhere in .rea text — authors reference APIs by alias only. This ensures all external access is declared, auditable, and permission-controlled.

json
{
  "title": "Weather Story",
  "allowed_urls": [
    {
      "alias": "weather",
      "url": "https://api.weather.example.com",
      "params": ["lat", "lng"]
    },
    { "alias": "maps", "url": "https://maps.example.com" }
  ]
}

Each entry in allowed_urls is an object with:

FieldTypeDescription
aliasstringShort name used to reference this API in .rea
urlstringBase URL prefix the story may access
paramsstring[]Optional list of allowed query parameter names

Authors reference allowed APIs by alias in story code. If a request fails, the runtime returns undefined and the story continues.