Skip to content

Error Code Specification

The diagnostics reported by the Kumiki compiler (@kumikijs/compiler) split into two families: parse errors and type-check errors. This document enumerates both normatively. If a code is added or changed on the implementation side, this document must be updated at the same time.

The Form of an Error

A type-check error is represented as a KumikiError:

ts
type KumikiError = {
  code: string;     // a stable identifier such as "E0103"
  kind: string;     // a machine-readable classification such as "undef-slot"
  message: string;  // a human-facing message (includes the target name)
  pos: Pos;         // { line, col }
  severity?: "error" | "warning"; // omitted ⇒ "error"
};

code is a permanent contract; once assigned, its meaning does not change. kind is a sub-classification under the same code, used to branch diagnostic logic. severity defaults to "error": a missing field means the same as "error" for backward compatibility with existing diagnostics. The "warning" tier is non-fatal — it is surfaced to stderr (CLI) and to Rollup's this.warn (Vite) but does not change the exit code or block the build.

A parse error is thrown as a ParseError (message + pos). Because the parse stage stops at the first error, no code is assigned.

The Code System

BandDomain
E00xxApp structure (such as mandatory routing requirements)
E01xxName resolution (undefined references)
E02xxType mismatch
E03xxCapabilities and purity
E04xxMotion
E06xxreducer write rules
E07xxAccessibility (a11y), strict-icons
E08xxRuntime hazards (code that compiles but breaks at runtime)
W02xxNon-fatal warnings (build still succeeds)

E00xx — Structure

E0001 missing-404

An app that declares app.routes must include a route for the /404 pattern. Unmatched paths fall back here.

app.routes must include a "/404" entry

Fix: Add a route to a 404 tile, such as route "/404" -> NotFound. See Routing for details.

E0002 duplicate-timer-name

Two or more timer(d, name=N) triggers declare the same timer name N. Timer names share one namespace and must be unique across the app, so that stop-timer(N) is unambiguous.

Timer name "<name>" is declared more than once

Fix: Rename one of the timers so each name= is unique. See timer.

E01xx — Name Resolution

E0102 undef-reducer

An event handler argument / prop refers to a reducer name that does not exist.

Reference to undefined reducer "<name>"

Fix: Check the spelling of the reducer name. kumiki fix can suggest a close name (→ AI Editing).

E0103 undef-ref / undef-slot

  • undef-ref: An undefined name was referenced in an expression.

    Reference to undefined name "<name>"

  • undef-slot: An assignment was made to an undefined slot in a reducer body.

    Assignment to undefined slot "<name>"

Fix: Confirm that the referenced slot / binding is declared.

E0104 undef-effect

The target of an emit refers to an undefined effect.

Reference to undefined effect "<name>"

E0106 undef-timer

A stop-timer(N) statement refers to a timer name N that no timer(d, name=N) trigger declares.

stop-timer refers to undefined timer name "<name>"

Fix: Check the spelling, or declare the timer with timer(d, name=N). See timer.

E0105 undef-tile

A tile reference, or the target of a route definition, refers to an undefined tile.

Reference to undefined tile "<name>"Route "<path>" targets undefined tile "<name>"

E0107 undef-motion

A tile's motion: "<name>" prop refers to a motion that no motion <name> = {…} definition declares.

Reference to undefined motion "<name>"

Fix: Check the spelling, or declare the motion. See The motion definition.

E0108 undef-member

A recv.member access where the inferred type of recv is known, but member is neither a field of that type nor a stdlib method/shortcut for it (ADR-002). This catches a typo (list.frist) or a member used on the wrong shape (record.head where the record has no head field). When the receiver type can't be inferred, no error is raised — the name-based shortcut dispatch is used instead.

Record type has no field or method ".<member>" / Type "<T>" has no member ".<member>"

Fix: Correct the member name, or — if recv is a record — use a field that exists. See List(T).

E0110 unknown-token-group

A @<group>.<name> theme-token reference (Style §4.3) names a <group> that is not one of the closed theme namespaces (colors, spacing, radius, shadow, typography, breakpoints).

Unknown theme token group "@<group>" (allowed: …)

Fix: Use one of the listed groups (e.g. @colors.surface, @spacing.md), or — if you wanted a plain identifier — drop the @ prefix.

E0109 test-wildcard-misuse

A test wildcard (<any-id> / <slots.X>) appears outside a reducer-test expect — in a reducer/tile/fn/app body, or in a test's given. Wildcards are a matching construct for the expected result only (Wildcards); they have no meaning as a value to compute or feed in.

Test wildcard "<any-id>" is only valid inside a reducer-test \expect``

Fix: Remove the wildcard, or move it into the reducer-test expect.

E0110 sub-routes-without-wildcard-parent

A tile declares sub-routes but its parent route in app.routes is not a wildcard (/*) pattern. Without a wildcard parent the runtime never reaches the nested matcher, so the sub-routes can never apply. See Nested Routes.

Tile "<name>" declares sub-routes but its parent route "<path>" is not a wildcard pattern (must end with "/*")

Fix: Change the parent route's pattern to end with /* (e.g. /settings/settings/*), or remove the sub-routes block.

E0111 orphan-sub-routes

A tile declares sub-routes but no entry in app.routes targets that tile. The nested route table cannot be reached.

Tile "<name>" declares sub-routes but is not the target of any route in app.routes

Fix: Add a route in app.routes that targets this tile with a /* pattern, or remove the sub-routes block.

E0112 duplicate-sub-route

The same sub-route path is declared more than once on a single tile. Match order is positional, so duplicates are either dead code or a typo.

Sub-route path "<path>" is declared more than once in tile "<name>"

Fix: Remove the duplicate entry, or rename one of the paths.

E0113 sub-routes-without-outlet

A tile declares sub-routes but its body never calls route-outlet. The matched child route would have nowhere to render — the page would silently miss its child content even though the compile succeeded.

Tile "<name>" declares sub-routes but its body never calls "route-outlet" — the matched child would have nowhere to render

Fix: Add a route-outlet() somewhere in the tile body where the child should appear, or remove the sub-routes block.

E02xx — Types

E0201 type-mismatch

An event handler argument / prop must be a reducer name, but was a different kind of value.

Event handler arg "<name>" must be a reducer nameEvent handler prop "<name>" must be a reducer name

E0204 effect-id-misuse

A value of type EffectId is used in an operation that is not defined on it. The only operations on EffectId are equality (==, !=), assignment to a slot of type EffectId, and being passed to an effect whose in type is EffectId. Arithmetic, ordering comparisons, and text(...) rendering are rejected — EffectId is opaque so the runtime can change its representation without breaking apps.

Operator "<op>" cannot be applied to EffectId — only "==" / "!=" are definedtext(...) cannot render EffectId — it is an opaque handle

Fix: Use == / != to compare against EffectId.none, or pass the value to a cancel effect. See EffectId.

E0205 bind-on-file-input

input(type="file") cannot bind a slot via bind=. The bind= two-way binding table (Forms §5.1.1) has no acceptable type for files — files are surfaced through the change event payload instead (Forms §5.10).

input(type="file") does not support bind="<name>"; receive files via a ui.change reducer with $event.files.head

Fix: Remove bind=, and add a reducer that picks the file from the event:

kumiki
slot avatar : Option(File) = None
tile AvatarPicker = input(type="file", accept="image/*")
reducer pickFile on=ui.change(AvatarPicker) do= avatar := $event.files.head

E0206 file-only-prop

The accept and multiple props on input apply only when type="file". They are rendered onto the underlying <input> element, where they are valid HTML only for a file picker (Forms §5.10). Used on any other input type — or when type is omitted (it defaults to "text") — they are invalid HTML and a latent bug. The diagnostic fires only when the type is statically known to not be "file"; a non-literal type= expression is left alone.

input prop "accept" requires type="file" (got type="text"); accept/multiple are only valid on file inputsinput prop "multiple" requires type="file" (got no type, defaults to "text"); accept/multiple are only valid on file inputs

Fix: Either add type="file" to opt into a file picker, or remove the accept / multiple prop:

kumiki
slot avatar : Option(File) = None
tile AvatarPicker = input(type="file", accept="image/*", multiple=true)
reducer pickFile on=ui.change(AvatarPicker) do= avatar := $event.files.head

E0207 pat-arity-mismatch

A match arm pattern's element count does not agree with the scrutinee's static type. Tuple patterns must have the same arity as their Tuple(...) scrutinee; variant patterns must have the same bind count as the variant's payload arity. Without this check, codegen would emit a guard that is always false at runtime — the arm would silently never fire ("compiles but the wrong branch runs").

Tuple pattern has <m> item(s) but scrutinee type "Tuple(<…>)" has <n>Variant "<tag>" pattern has <m> bind(s) but the variant carries <n> payload(s)

Fix: Add or drop pattern elements to match the type. For Tuple(Int, Int), write (a, b). For Some(T), write Some(x) (one bind) rather than Some(x, y).

E0208 pat-type-mismatch

A match arm pattern's shape does not agree with the scrutinee's static type — for example, a tuple pattern (a, b) is used against a scrutinee of type Int, or a variant pattern Some(x) is used against a record type. The pattern can never structurally match, so the arm is dead at compile time.

Tuple pattern cannot match scrutinee of type "<T>"Variant pattern "<tag>" cannot match scrutinee of type "<T>"

Fix: Use a pattern whose shape matches the scrutinee. If the scrutinee really should be a union or a tuple, fix its declared type instead.

E0209 pat-unknown-variant

A match arm names a variant tag that is not declared by the scrutinee's union type. Built-in unions (Option(T) admits Some / None; Result(T, E) admits Ok / Err) and user-declared type X = A | B(…) | … definitions are both checked.

Variant "<tag>" is not a member of scrutinee type "<T>"

Fix: Correct the tag spelling, or add the variant to the union definition. kumiki fix can suggest a close name (→ AI Editing).

E0210 type-arity-mismatch

A type-level application T(...) of a user-declared generic type passes a different number of type arguments than the declaration's parameters. Without this check, downstream type-param substitution would silently produce a short map and leave unresolved TypeRefs in payloads, which would then turn pattern checking into a no-op — exactly the silent-failure shape this error band is meant to catch.

Type "<name>" expects <m> type argument(s) but got <n>

Fix: Adjust the call site to pass the declared number of type arguments, or change the declaration's parameter list.

E0211 undef-tile-in-selector

A reducer's ui.* selector names a tile that has not been declared. Without this check a typo (ui.click(SaveBtn) vs ui.click(SaveBtnn)) compiles silently and binds nothing — indistinguishable from a deliberately unused reducer.

Reducer "<name>" subscribes to ui.<ev>(<Tile>) but tile "<Tile>" is not declared

Fix: Add a tile <Tile> = … declaration, or correct the selector's tile name to match an existing one. The _ wildcard (for reducers dispatched indirectly via emit confirm({onYes: r, …}) callbacks, see Lifecycle §7) is accepted and has no tile to resolve.

E0212 selector-id-mismatch (opt-in via --strict-selector-id)

A reducer's ui.<ev>(Tile#id) selector names an id that the target tile's literal {id: "..."} prop cannot produce. E0211 catches typos in the tile name; this catches typos in the #id fragment — e.g. on=ui.submit(NewForm#nw) against tile NewForm = form(...) {id: "new"}. The runtime _dispatch filter (spec §1.6.2) silently skips the mismatch, so without this check the reducer never fires and the developer sees no error. Opt in with kumiki check --strict-selector-id or compile({ strictSelectorId: true }).

Reducer "<name>" subscribes to ui.<ev>(<Tile>#<id>) but tile "<Tile>" is declared with id "<actual>" — this selector can never match

The checker descends into all four control-flow bodies (for / when / if / match): for / when pass through to their single body, if's two branches merge, and every match arm contributes to the observed id set. tile T = if c then button(...) {id: "a"} else button(...) {id: "b"} produces "a" | "b"; a selector T#c under --strict-selector-id fires E0212. Referenced user tiles are NOT descended — a Ref to another tile leaves the id set unknown, so a future per-instance id-override syntax at the use site isn't foreclosed at compile time.

When E0212 stays silent (runtime filter is authoritative):

  • The tile has no {id} prop at all.
  • The tile's {id} value is a non-literal expression (a Ref, method call, etc.) — its runtime value is not known at check time.
  • The selector has no #id portion.
  • The selector uses the _ wildcard.
  • The target tile is undeclared (E0211 already fires; E0212 is suppressed to surface a single root cause).

Fix: Correct the #id in the selector to match the tile's declared {id}, or correct the tile's {id} literal.

W0212 ui-event-tile-mismatch (warning)

A reducer subscribes to ui.<ev>(<Tile>) whose target tile has no descendant that fires <ev> in the DOM — e.g. ui.focus(Card) where tile Card = box(...). Codegen silently drops the handler, so the reducer is dead code. Lifting that into a warning surfaces the silent failure at check time without breaking the build. The checker walks the tile's body (including child tiles), so a cascade pattern like TodoRow = row(check(...), …) + ui.click(TodoRow) does NOT trigger the warning — codegen routes the handler to the focusable descendant.

Reducer "<r>" subscribes to ui.<ev>(<Tile>) but tile "<Tile>" has no descendant that fires "<ev>" (DOM-allowed: …; observed in body: …). The handler is silently dropped.

The allowed root builtins per event are (current toolchain coverage; the implementation-side source of truth is packages/compiler/src/ui-lifts.tsUI_LIFTS, which both codegen.ts's handler-emission gate and the W0212 check derive from. Runtime DOM-event surfaces are owned by packages/runtime/src/tiles-input.ts and the universal applyUiEventHandlers in core.ts):

ui.<ev>allowed root tile kinds
clickbutton, check, switch, radio
submitform
changeselect, input, textarea, check, radio, switch, slider
inputinput, textarea
keyinput, textarea, button
focusinput, textarea, button, select
blurinput, textarea, button, select
hoverany tile

Fix: Re-target the selector at a tile whose root is in the allowed set, or wire the handler explicitly on the focusable element (input(onFocus=r)). The wildcard _ selector and the ui.hover event are exempt.

The checker descends into control-flow bodies (for / when / if / match) too: both if's then/else and every match arm contribute to the observed kind set. So tile Dyn = for n in xs box(...) triggers W0212 (only box reachable), while tile T = if c then input(...) else button(...) does not (both branches contribute an allowed root). A tile whose body is entirely unresolvable (cycle, or a name no other tile defines) yields an empty observed set and the warning is suppressed — better silent than wrongly accusing.

Note on link: link is intentionally not listed under click even though <a> fires click natively — the runtime reserves the click event on links for navigation interception and does not invoke user onClick reducers. Re-targeting a button or wiring onClick= on a parent tile is the current workaround.

E03xx — Capabilities and Purity

E0301 missing-capability

A capability required by an effect is not declared in app.caps.

Effect "<effect>" requires capability "<cap>" which is not declared in app.caps

Fix: Add the required capability to app.caps. For details on the capability model, see Lifecycle.

E0302 unknown-capability

An entry in app.caps is neither a standard capability (Standard Capabilities) nor one registered in a kumiki.caps.json manifest.

Unknown capability "<name>" in app.caps — use a standard capability or register it in kumiki.caps.json

Fix: Use a standard capability, correct the spelling, or register the custom capability in a kumiki.caps.json next to the .kumiki file. See Standard Capabilities.

E0303 invalid-cancel-target

An effect declared with cap=http.cancel does not have the required shape in=EffectId out=Unit, or declares attributes the cancel path silently ignores (policy, retry, map-request). The cancel capability cancels by id and returns nothing; declaring per-request behaviour on it is a user-intent mismatch.

effect "<name>" with cap=http.cancel must declare in=EffectId out=Uniteffect "<name>" with cap=http.cancel cannot declare a policyeffect "<name>" with cap=http.cancel cannot declare retryeffect "<name>" with cap=http.cancel cannot declare map-request

Fix: Change the in= and out= clauses to in=EffectId out=Unit, drop any policy= / retry= / map-request= clauses, or remove the cap=http.cancel clause. See HTTP Cancellation.

E0305 fn-impurity

A fn (pure function) is reading a slot. A fn must depend only on its arguments.

fn "<name>" must not read slot "<name>"

Fix: Pass the required slot value as an argument.

E04xx — Motion

Validity of a motion definition's closed grammar (The motion definition).

E0401 motion-unknown-property

A keyframe stop uses a property outside the closed animatable set (opacity, translate-x, translate-y, scale, rotate), or gives one a non-number value.

motion "<name>": unknown keyframe property "<prop>" (allowed: …)

Fix: Use a supported property, or express the effect with one.

E0402 motion-invalid-timing

A timing field is outside its closed set: duration (a number of ms or fast/normal/slow), easing (linear/ease/ease-in/ease-out/ease-in-out), iteration (a positive Int or infinite), direction (normal/reverse/alternate/alternate-reverse) — or the field name itself is unknown.

motion "<name>": easing must be one of …

Fix: Use a value (or field) from the closed set.

E0403 motion-malformed

A motion is missing its keyframes record, or the keyframes lack a from / to stop (or use a stop other than from / to).

motion "<name>" keyframes must include a "to" record

Fix: Provide keyframes: {from: {…}, to: {…}}.

E06xx — reducer Write Rules

E0601 duplicate-write

Within the same reducer, the same slot path shape (lvalue shape) is written more than once. This violates the one-write-per-reducer rule (at path-shape granularity).

Slot path "<shape>" is written more than once in this reducer

Note: The granularity is path shape. issues[id].status and issues[id].updatedAt are considered different shapes and can coexist, but double assignment to count is forbidden.

E07xx — Accessibility (a11y) and strict-icons

A band for checks that ship as warnings by default and are promoted to errors via an explicit strict* opt-in. check() filters these codes out unless the matching flag is set.

a11y checking is enabled via check(program, { strictA11y: true }).

E0701 a11y-button

button must have a text= argument or aria-label prop

E0702 a11y-image

image must have an alt prop

link must have inner text or aria-label

Fix: Provide visible text, or an aria-label / alt. For general guidance on forms, see Forms.

Strict-icons checking is enabled via check(program, { strictIcons: true, iconNames }).

E0704 unknown-icon

Unknown icon name "<x>" — not in @kumikijs/icons or any theme.icons block

A literal icon(name="<x>") reference whose name is not in the iconNames set passed to check() (typically the keys of @kumikijs/icons's ALL_ICONS) and is not declared in any theme.icons block in the source. Dynamic icon(name=<expr>) calls are never checked — the name is unresolvable at check time and falls through to the runtime placeholder (see Style §4.8.4).

Fix: Correct the typo, register the custom path in theme.icons, or install @kumikijs/icons so the built-in name is in scope.

E08xx — Runtime Hazards

A band for statically catching, at the check stage, "code" that passes type checking but breaks at runtime. For the three-layer verification model, see The Three Layers of Tooling Verification.

E0801 unimplemented-method

A method call of the form obj.method(...) does not exist in the set of methods implemented by the runtime / code generation. This occurs from a misspelling (.fitler), a method that appears in the specification but is unimplemented, or misuse of a method from a different type (such as .to-result on Option).

Method ".<name>" is not implemented by the runtime

Note: The set of implemented methods is solely authoritative in @kumikijs/compiler's KNOWN_METHODS (kept in sync with code generation's methodCallJs). Calling a no-argument method with () is also caught by this band. For the list of standard library methods, see Standard Library.

Fix: Correct it to the right method name, or rewrite the operation using implemented means such as match / fold. If you need an unimplemented specification method, implement it in packages/ and add a working example in examples/.