This is what a crash from a Roku channel looks like when it reaches us:

RuntimeError(&hEC)  'Dot' Operator attempted with invalid BrightScript Component or interface reference.

1  labcrashnow   pkg:/components/LabScene.brs:171
2  onkeyevent    pkg:/components/LabScene.brs:123

-20.0 s  NAV  screen: Lab
+25.5 s  NAV  screen: DetailsScreen
+25.5 s  TAP  key OK
+27.0 s  TAP  key options

A runtime error, two frames, and the last four things the viewer did: opened the details screen, pressed OK, pressed the options key. Nobody was connected to the device over telnet.

We have just released the Roku SDK. This page is a run through its test channel: a test channel with ten scenarios, what each one produced in the dashboard, and the cases where a Roku crash log will still arrive without a stack.

Why Roku crash logs are hard to get

In day-to-day Roku app development, a device in developer mode prints the crash to the BrightScript debug console. You telnet to port 8085, read the backtrace, and inspect variables in the BrightScript debugger. That is Roku debugging as most channel developers know it, and it works well for the one device you are holding.

A published channel has none of that. When a viewer’s channel hits an uncaught runtime error, it exits to the home screen. There is no dialog, no console you can reach, and the process that could have sent a report is gone.

Roku does keep one thing. Since Roku OS 13.0, roAppManager.GetLastExitInfo() returns why the previous launch ended: an exit code such as EXIT_BRIGHTSCRIPT_CRASH, a timestamp, and a short tail of the console. Three limits shape everything that follows:

  1. It is only available on the next launch. A crash is reported when the viewer opens the channel again, which can be days later.
  2. Roku gives it only to the channel's own code. Called from inside a ComponentLibrary, it answers EXIT_UNKNOWN with no timestamp. An SDK loaded as a library cannot read it for you.
  3. The console tail is 20 lines. It is frequently filled with beacon lines, so the frames you wanted are often not in it.

So there are two jobs. Read the exit record in Main() and hand it to something that can send it. And catch errors where they are thrown, so the stack does not depend on those 20 lines.

The setup: a channel built to crash

The test channel is examples/roku-channel in the SDK repository. It is a list of ten scenarios and a status panel. No content, no video.

The Everframe crash lab running on a Roku: ten scenario rows on the left and an SDK status panel on the right.
The crash lab on a Roku. The status panel shows the library loaded, the SDK started, and nothing queued.

The SDK is an npm package that carries a ComponentLibrary zip and a small CLI:

npm i -D @everframe/roku

The scene declares the library and starts it once it has loaded:

<!-- MainScene.xml: <version> is the installed package version -->
<children>
  <ComponentLibrary id="Everframe" uri="pkg:/components/everframe-roku-<version>.zip" />
</children>
' MainScene.brs
sub init()
    m.efLib = m.top.findNode("Everframe")
    m.efLib.observeField("loadStatus", "onEverframeLoad")
    ' A bundled library can already be "ready" here; the observer only sees later changes.
    onEverframeLoad()
end sub

sub onEverframeLoad()
    if m.efLib.loadStatus <> "ready" or m.efStarted <> invalid then return
    m.efStarted = true
    ef = CreateObject("roSGNode", "Everframe:Everframe")
    ef.callFunc("start", { sdkKey: "evf_live_..." })
end sub

That much gives you captureException, breadcrumbs and a user. It does not yet detect a crash that ends the channel, because of the second limit above: the exit record has to be read by your own Main(). The install guide has those lines, about fifteen of them, which store the record in the registry for the SDK to pick up after start().

We did not paste them, because the build step adds them.

The build step that adds the stack

everframe-roku instrument ./ --out ./.everframe-build --bundle-library

This writes an instrumented copy of the channel to .everframe-build and leaves your sources alone. For the crash lab we added two --exclude flags, for Excluded.brs and OomTask.brs, to see what a crash looks like when nothing wraps it. It printed:

wrapped  source/main.brs  Main
wrapped  components/CrashTask.brs  init
wrapped  components/CrashTask.brs  crashInTask
wrapped  components/DetailsScreen.brs  init
wrapped  components/LabScene.brs  init
wrapped  components/LabScene.brs  onLibraryStatus
wrapped  components/LabScene.brs  onItemSelected
wrapped  components/LabScene.brs  onKeyEvent
wrapped  components/LabScene.brs  onStatusTick
wrapped  components/LabScene.brs  onLoopTick
screen   components/DetailsScreen.brs  DetailsScreen  via DetailsScreen
library  components/everframe-roku-1.2.0.zip
10 function(s) wrapped, 3 component(s) hooked, 1 screen(s) tracked

Each wrapped function body goes inside try ... catch e ... end try on the same source line. The catch records the error with its backtrace and re-throws the original exception. A crash that would have ended the channel still ends it, and an error your own code catches further up is still caught there.

Three things come with the wrapping: Main gets the exit-record lines, onKeyEvent leaves a breadcrumb per key press, and any component named *Screen, *View or *Page sets the current screen in its init(). That is where screen: DetailsScreen in the trail above came from. Nobody called setScreen.

Eight error groups

The scenarios ran on one Roku, with a relaunch after each crash. The dashboard grouped the results into eight errors.

Everframe errors list for the Roku integration, showing eight groups: six BrightScript runtime errors and two exit codes.
Errors for the Roku integration. Four of the eight share a message and are still separate groups, because their frames differ.
ScenarioWhat arrivedStack
Options key in onKeyEventRuntimeError(&hEC), thread render, mechanism try-catchlabcrashnow ← onkeyevent
Crash inside a TaskRuntimeError(&hEC)crashintask at CrashTask.brs:10
Crash before the scene existsRuntimeError(&hEC)labcrashbeforescene ← main
Crash loop, four launchesRuntimeError(&hEC), 3 occurrenceslabcrashnow ← onlooptick
Handled errorRuntimeError(&h28) “Crash lab handled error”labreporthandled ← onitemselected
Crash in an excluded fileEXIT_BRIGHTSCRIPT_CRASH, mechanism exit-infonone
Out of memory in a TaskEXIT_CHANNEL_MEM_LIMIT_FG, mechanism exit-infonone
Not a scenarioRuntimeError(&h18) Type Mismatchupdatestatus ← onlibrarystatus

The crash-loop row is the guard working. The scenario crashes on four consecutive launches. The device sends a given fingerprint once per launch and at most three times in a rolling hour, and the group shows three occurrences.

The last row was not on the list. More on it below.

Reading one crash

The report at the top of this page is the options-key scenario. In the lab, row 10 opens a second screen, and pressing the options key there calls a method on invalid.

The crash lab's details screen on a Roku. The status panel reads Screen: DetailsScreen and User: 42 lab@example.com.
One key press before the crash. The status panel already reads Screen: DetailsScreen, set by the instrumented init().
Everframe error group for a BrightScript 'Dot' Operator runtime error with a two-frame stack trace and a four-entry trail.
The error group. Frames on top, innermost first. The trail below them is the session that crashed.

The Context tab fills in the rest. For this occurrence it read:

FieldValue
ContextonKeyEvent (components/LabScene.brs)
Threadrender
Mechanismtry-catch
RouteDetailsScreen
Exit reasonApp crashed (BrightScript error) EXIT_BRIGHTSCRIPT_CRASH

Two mechanisms show up on one report here. The try-catch wrapper recorded the error when it was thrown. The channel then died, and on the next launch the exit record said EXIT_BRIGHTSCRIPT_CRASH. The SDK matches the two and files one report, not two. It matches them by kind, not by clock, because the device clock and the exit timestamp can differ by tens of seconds.

The same wrapper works off the render thread. The Task scenario arrived with a single frame, crashintask at pkg:/components/CrashTask.brs:10, and the crash in Main() arrived with labcrashbeforescene ← main, even though no Everframe node existed yet when it happened. The hook wrote the record straight to the registry and the launch after sent it.

Crashes that arrive without a stack

Row 6 crashes in Excluded.brs, one of the two files left out of instrumentation. Nothing catches the error. The only evidence is the exit record on the next launch.

That report has the exit code as its type, EXIT_BRIGHTSCRIPT_CRASH, and the dashboard says plainly: no stack trace on the latest occurrence. It still carried the trail (screen: Lab, key OK) and the route, because the SDK writes the latest 20 breadcrumbs and the current screen to the registry as it goes, and the next launch attaches the ones from the session that ended.

This is what every crash looks like if you skip the build step and add the Main() lines by hand. You learn that the channel crashed, on which screen and on which OS version. You do not reliably learn where. Skip both, and a crash that ends the channel is not reported at all. That is the argument for instrumenting release and beta builds.

Out of memory, with the reading before it

An out-of-memory kill never throws. The OS ends the channel. No try/catch can see it, so this one is exit-info only by nature.

Everframe occurrence details for EXIT_CHANNEL_MEM_LIMIT_FG: out of memory in foreground, the last memory reading, mechanism exit-info.
The out-of-memory group. No stack, but the exit reason, the app state and the last memory reading are there.

Where roAppMemoryMonitor is available, the SDK checks memory use every 5 seconds and leaves a breadcrumb the first time it crosses 75, 90 and 95 % of the channel’s limit, plus one for the OS warning event. Memory breadcrumbs are written to the registry at once, because the kill may follow within moments. The trail on this report was three lines:

CUSTOM  memory <n>% of <limit> MB
CUSTOM  memory warning from OS (<n>%)
CUSTOM  memory warning from OS (<n>%)

Three entries, then the kill. The lab allocates memory faster than a 5-second poll can follow, so a slower leak will give you more steps than this. The limit itself differs by device.

The crash we did not plan

The eighth group is RuntimeError(&h18), Type Mismatch. Operator "+" can't be applied to "String" and "Invalid"., with frames updatestatus ← onlibrarystatus.

That is not one of the ten rows. It is the lab’s own status panel, called from the library-status observer, concatenating a string with a value that was invalid. The channel written to test crash reporting had a real crash of its own, and it showed up in the dashboard next to the planned ones.

What it does not do

Roku is the one Everframe SDK that only reports crashes and errors. There is no in-channel reporter, no screenshot, no session replay, and no console or network capture. A viewer or tester cannot file a report from the channel. Network and playback breadcrumbs exist only if you add them with addBreadcrumb.

Other limits worth knowing before you ship it:

  1. Reports are late by design. A crash is sent when the channel is next opened. occurredAt is the exit time and submittedAt can be days after it. Order on occurredAt.
  2. Exit detection needs Roku OS 13.0. Older firmware has no GetLastExitInfo. Instrumented try/catch works from Roku OS 9.4.
  3. It changes the debugger. In an instrumented build the debugger stops on the wrapped function's end sub, with the failing line's locals already gone. Keep development builds uninstrumented.
  4. It uses the registry. Up to 6 queued reports, the breadcrumbs and the screen take about 14 KB of Roku's 16 KB. Budget your own registry data around that.
  5. Some functions are skipped. A function containing a goto label cannot go inside a try block. The CLI lists every one it leaves alone.

What this run did not cover: the reports above came from one Roku model on one OS version, over a LAN, from a sideloaded build. We have not measured a published channel at volume, and the crash lab has no video playback in it.

If you are setting this up

  1. Install and start. npm i -D @everframe/roku, declare the ComponentLibrary, call start() when it is ready. See Install.
  2. Run instrument --dry-run first. It prints what would be wrapped and which components would be tracked as screens, and writes nothing.
  3. Instrument release and beta deploys only. Point roku-deploy at .everframe-build from inside that folder. See Build instrumentation.
  4. Crash it once on purpose. Call a method on invalid behind a hidden key, relaunch, and check the group has frames. Then read Crash reporting for what each field means.

The SDK is MIT-licensed and on npm as @everframe/roku. The Roku SDK page lists what is and is not captured.