Gotchas & common compile errors

The fastest way to stop fighting the compiler. Each row is a mistake that costs a build round-trip, the diagnostic code you’ll see (banto.<code>), and the fix. Read this once before writing a game; skim it again the first time a build fails.

Source of truth for the rules is SYNTAX-AND-USAGE.md; this page is the “things people actually trip on” subset. Diagnostic codes come from packages/core/src/diagnostic-codes.ts.

Project & state structure

SymptomCodeFix
Build says no start statemissing-start-bantoThe project must have a start.banto whose state block is named start.
A state renders nothing / errors on entrydefault-host-missing, default-plyr-missingEvery state needs one host default { condition: true, … } and one plyr default { condition: true, … }. These are the fallback screens.
”default must be last” / “default condition must be true”default-not-last, default-condition-not-trueThe default block goes last among its host/plyr siblings, and its condition must be literally true. Put conditional variants above it.
lstn ... next can’t find a statestate-not-foundThe next.state string must match a real state name. States are files: foo.bantostate foo.
next complains about inputslstn-next-missing-inputs, lstn-next-inputs-mismatch, lstn-next-extraneous-inputsIf the target state declares params, pass exactly those (names + types) in next.inputs.

Where blocks are allowed

SymptomCodeFix
cpnt / data / func / global var rejected in a state filecpnt-only-in-globals, data-only-in-globals, func-only-in-globals, block-not-in-globalsDeclare custom components, datasets, custom funcs, and global vars in globals.banto. State files hold state, host, plyr, lstn, and state-local var/cstate.
cstate rejectedcstate-only-in-statescstate (client-local draft state) lives inside a state block, not globals.

Actions, client vs server

SymptomCodeFix
Action arrow rejected for too many paramsaction-arrow-too-many-paramsAction arrows take at most one parameter: arg => { … }. Components pass a single value (onChange: (v) => …).
client.X rejectedclient-func-not-in-action, client-func-in-loopclient.* (e.g. client.playSound) may appear only inside an action arrow body (onClick/onChange/onSubmit), and never inside a .map/.forEach/.filter body.
cstate write rejectedcstate-write-not-top-level, cstate-write-interleaved, client-phase-mixed-branchcstate.* writes must sit at the top level of an action body and not be interleaved with server writes (var.*, func.notify, …). Do all cstate writes together; don’t mix a cstate write and a var write in the same if/forEach branch.
Nothing happens when a button is clicked(no error)The action must sit directly in a component’s handler slot (onClick: () => …). An action nested inside a plain-object data param is silently dropped — hoist it to the handler field.
Text/number input loses characters or won’t submit(no error)Bind value to a cstate.<name>, write cstate.<name> = arg in onChange, and commit to var.* (and clear the cstate) in onSubmit. Binding value straight to a var fights the server round-trip.

Iterators & registry functions

SymptomCodeFix
.map(...) callback rejectediterator-callback-statement-body.map/.filter callbacks must be a single expression (x => x.score), not a { … } statement block. .forEach is the one that takes a statement body.
Passing an arrow to func.mapValues / func.groupBy failscall-arg-type-mismatchRegistry funcs don’t accept arrow callbacks. mapValues/groupBy take a key string (a property name), not a transform function.
break / continue rejectedbreak-continue-outside-loopOnly valid inside a .forEach body.
Object.assign / Object.freeze / etc. rejectedunknown-funcBanto recognizes only Object.keys, Object.values, Object.entries, Object.fromEntries. Everything else is a compile error.
A list of components renders empty for a non-named chain root(fixed in compiler)[1,2,3].map(...) etc. now compile to a chain IR node and iterate correctly — you don’t need a named intermediate.

Custom func.X

Custom funcs are pure helpers over their params — think of them as stateless utilities.

SymptomCodeFix
Func body references var/cstate/state/data/playerfunc-scope-forbiddenPass game state in as an argument (func.score({players: var.players})), don’t read it from scope.
Writing to params.x inside a funcfunc-param-readonlyParams are read-only. Build a new value and return it.
Func calls itselffunc-recursionRecursion is disallowed. Flatten to a loop / func.range(...).forEach(...).
Return-type errorsfunc-missing-return, func-return-in-void, func-return-type-mismatchIf returnType is set, every path must return an assignable value; if it’s unset, don’t return a value.

CSS

styles.banto.css is a restricted subset. css-forbidden-property, css-forbidden-function, css-forbidden-atrule, and css-forbidden-element-selector mean you used something outside the allowlist — styling has to be done via a css block reference (declared in styles.banto.css)

Custom assets (asst.X)

SymptomCodeFix
asst.<name> rejected as unknownunknown-assetThe base name must match a file you’ve pushed. Add it under assets/, title it in banto.config.json, and run banto assets push.
Image asset in an audio slot (or vice versa)(type error)asst.<name> carries a kind. cpnt.image wants an image, cpnt.audio/client.playSound want audio. Push the right file and reference it.

Runtime (no compile error, but wrong on screen)

  • Host overflow is a bug. The host screen shows on a TV and must never scroll in any direction. Size fonts/padding so the busiest phase (max players, longest text) still fits.
  • An empty dataset means you didn’t seed one. When you host a published game, data.questionSet/promptSet/dynamic come from the data source the host picks at game start — which is your banto.config.json default.* binding. If default.* is missing (and there’s no new.* seed to bind "this" to), the dataset is empty. Ship a new.* seed or point default.* at a real data-source id.
  • A custom asset renders nothing. If a cpnt.image/cpnt.audio shows nothing, the asst.<name> either wasn’t pushed (banto assets push) or you passed a raw string id/URL that failed to load. asst.<name> refs are build-checked; raw strings are not.
  • func.pickRandom / func.first / func.last return undefined on an empty list. Branch before use. Number aggregates (sum/avg/minOf/maxOf) return 0 on empty instead, so they render cleanly.