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
| Symptom | Code | Fix |
|---|
| Build says no start state | missing-start-banto | The project must have a start.banto whose state block is named start. |
| A state renders nothing / errors on entry | default-host-missing, default-plyr-missing | Every 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-true | The 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 state | state-not-found | The next.state string must match a real state name. States are files: foo.banto → state foo. |
next complains about inputs | lstn-next-missing-inputs, lstn-next-inputs-mismatch, lstn-next-extraneous-inputs | If the target state declares params, pass exactly those (names + types) in next.inputs. |
Where blocks are allowed
| Symptom | Code | Fix |
|---|
cpnt / data / func / global var rejected in a state file | cpnt-only-in-globals, data-only-in-globals, func-only-in-globals, block-not-in-globals | Declare custom components, datasets, custom funcs, and global vars in globals.banto. State files hold state, host, plyr, lstn, and state-local var/cstate. |
cstate rejected | cstate-only-in-states | cstate (client-local draft state) lives inside a state block, not globals. |
Actions, client vs server
| Symptom | Code | Fix |
|---|
| Action arrow rejected for too many params | action-arrow-too-many-params | Action arrows take at most one parameter: arg => { … }. Components pass a single value (onChange: (v) => …). |
client.X rejected | client-func-not-in-action, client-func-in-loop | client.* (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 rejected | cstate-write-not-top-level, cstate-write-interleaved, client-phase-mixed-branch | cstate.* 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
| Symptom | Code | Fix |
|---|
.map(...) callback rejected | iterator-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 fails | call-arg-type-mismatch | Registry funcs don’t accept arrow callbacks. mapValues/groupBy take a key string (a property name), not a transform function. |
break / continue rejected | break-continue-outside-loop | Only valid inside a .forEach body. |
Object.assign / Object.freeze / etc. rejected | unknown-func | Banto 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.
| Symptom | Code | Fix |
|---|
Func body references var/cstate/state/data/player | func-scope-forbidden | Pass game state in as an argument (func.score({players: var.players})), don’t read it from scope. |
Writing to params.x inside a func | func-param-readonly | Params are read-only. Build a new value and return it. |
| Func calls itself | func-recursion | Recursion is disallowed. Flatten to a loop / func.range(...).forEach(...). |
| Return-type errors | func-missing-return, func-return-in-void, func-return-type-mismatch | If 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)
| Symptom | Code | Fix |
|---|
asst.<name> rejected as unknown | unknown-asset | The 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.