Quick answer: PICO-8 surfaces crashes as Lua runtime errors printed to the console, which players rarely copy down. Wrap your update and draw loops so an error string and a small slice of game state can be reported, group duplicate reports by the error text, and triage them in one dashboard instead of chasing screenshots of red text.
PICO-8 is a fantasy console with deliberately tight constraints: a token budget, a 32k character source limit, limited memory, and a Lua runtime that halts with a terse red error line when something goes wrong. Those constraints are part of the charm, but they make crash reporting awkward. A player who hits nil index in your exported HTML or binary cart sees a cryptic message and usually just closes the tab. This post covers how PICO-8 crashes actually surface, why they are hard to reproduce, and how to capture enough context to fix them without blowing your token count.
How PICO-8 surfaces a crash
When Lua hits a runtime error in PICO-8, the cart stops and prints a line like runtime error line 42 tab 1 attempt to index a nil value. In the editor you see it immediately, but in an exported web or binary build the player just sees the game freeze on a red message over a black screen. There is no stack trace in the classic sense, only a line and tab reference, so reconstructing the path that led there depends entirely on knowing what the player was doing.
The most common culprits are nil indexing after an entity is removed from a table, arithmetic on nil when a variable was never initialized, and stack overflows from accidental recursion in a coroutine. Because PICO-8 favors terse global state and flat tables, a single bad index can ripple. Without a way to capture the error string and the surrounding state, you are left guessing from a one-line screenshot a player may or may not have bothered to send you.
Why these bugs resist reproduction
PICO-8 games lean on global variables, frame counters, and table churn for spawning and despawning actors. A crash often depends on a precise combination: an enemy deleted on the same frame a projectile resolves against it, or a level index that overruns a map region near the token-saving edge of your data. These race-like conditions in a single threaded 30 or 60 fps loop are timing sensitive and rarely line up the same way twice on your machine.
Exported carts add platform variance. The same cart runs in the web player, as a native binary, and inside Splore, and players hit it on wildly different hardware and browsers. Memory pressure near the 2 megabyte Lua limit or the compressed code size ceiling can behave differently across these targets. You need the error text plus a snapshot of the relevant globals at the moment of failure to turn a vague report into something you can actually step through in the editor.
Instrumenting the update and draw loops
PICO-8 does not give you try and catch, but you can guard the entry points. Rename your real logic to functions like _real_update and _real_draw, then have _update call them indirectly so that if you maintain your own dispatch you can record the last action, current state name, room index, and frame counter into a small table before each call. When the runtime halts, that breadcrumb table holds the last known good values, which you can serialize into a short string for reporting.
Keep the breadcrumb cheap. A handful of fields, the active scene, player position, and the last input matter most. Avoid dumping entire entity tables; you have neither the tokens nor the report size budget for that. The goal is a compact context string, perhaps under two hundred characters, that pairs with the error line and tab so you can jump straight to the offending code and recreate the conditions that produced the nil.
Getting the report off the device
A pure cart cannot make arbitrary network calls, so the practical path depends on your distribution. For web exports you can wrap the generated HTML and use the surrounding JavaScript to post the captured context to an endpoint, or expose a hook the player clicks to copy a report. For binary and itch downloads, the cleanest approach is an in-game report prompt that shows the error and a short code or asks the player to paste the context into a web form you link from the pause menu.
Whatever the transport, the design principle is the same: capture at the moment of failure, then deliver when a connection exists. Store the error string and breadcrumb in a buffer, show the player a friendly message instead of raw red text, and offer a single action to send it. That converts a silent abandon into a usable report, and it respects both the fantasy console aesthetic and the player who just wants to keep playing.
Setting it up with Bugnet
Bugnet works well for PICO-8 because it does not assume a heavy native runtime. For web exports you point the wrapper JavaScript at Bugnet so the captured Lua error string, the breadcrumb context, and basic platform info post as a crash report with a stack-style trace built from your line and tab references. For binary builds, an in-game report button or pause-menu link opens a Bugnet web form prefilled with the error code, so the player only confirms and sends. Either way the cart stays small and the heavy lifting happens outside the token budget.
Once reports arrive, Bugnet folds duplicates together. The same attempt to index a nil value at line 42 tab 1 across hundreds of players becomes one issue with an occurrence count instead of hundreds of red screenshots. You can attach custom fields such as cart version, export target, and active room, then filter to see whether a crash is web only or hits native builds too. That grouping turns the fantasy console blind spot into a prioritized list you can clear one fix at a time.
Building a crash budget into your workflow
Treat error handling as part of your token budget from the start rather than bolting it on near release. A small, fixed breadcrumb table and a single report path cost far fewer tokens than scattered defensive checks everywhere, and they pay off the first time a player on a browser you never tested hits an edge case. Decide early which globals are worth capturing and keep that list short so the instrumentation does not creep into your gameplay code.
Before you publish to the BBS, itch, or a binary export, deliberately trigger a few crashes and confirm the report makes it all the way to your dashboard. Test each export target, since the web player and native binaries can diverge. With a tested capture path and grouped reports, a constrained PICO-8 project becomes maintainable: you ship, you watch the occurrence counts, and you fix the loudest nil first instead of waiting for someone to screenshot a frozen cart.
PICO-8 crashes are just one-line Lua errors until you pair them with a tiny breadcrumb. Capture the state, group the duplicates, and the red screen stops being a dead end.