⏱️

Timer statements
setTimeout / setInterval / clearTimeout / clearInterval — JS-shaped Timer sugar.

setTimeout(<callback>, <ms>) and setInterval(<callback>, <ms>) are bare global functions — not namespaced like taskManager.run(...) — that generate and hide the full BrightScript Timer node lifecycle (creation, field wiring, ObserveField, start, cleanup) behind one call. <callback> can be an anonymous function or a named function reference.

<script>
state ready: boolean = false

public function setup() {
  setTimeout(function() {
    state ready = true
  }, 1500)
}
</script>

Milliseconds, not seconds

True JS parity — setTimeout(fn, 1000) fires after 1 second. Roku's own Timer.duration field is in seconds; the conversion happens for you, at compile time when the duration is a literal.

setTimeout(fn, 1000)   ' fires after 1 second — milliseconds, JS-shaped
setInterval(fn, 500)   ' fires every 0.5 seconds

Handles, setInterval, and clearing

Both calls return a handle — capture it (a plain local or an m.<field>) to cancel later with clearTimeout(<handle>)/clearInterval(<handle>). A setInterval callback is called with no arguments, repeatedly, until cleared.

<script>
public function setup() {
  m.pollHandle = setInterval(onPoll, 500)
}

private function onPoll() {
  state runningCount = taskManager.runningCount
}

private function stopPolling(key: string, press: boolean) {
  if (press) {
    clearInterval(m.pollHandle)
  }
}
</script>

Automatic cleanup on unmount

Every pending timer a component creates is automatically stopped when that component's own node is removed — navigating away from a router-mounted screen, an ancestor {#if:destroy} tearing down a subtree that contains it, or an {#each} block removing an item that contains it — any nesting depth, in all three cases. You don't need to call clearTimeout/clearInterval yourself just to avoid a timer outliving its component — call them only when you want to cancel early, while the component is still mounted.

This same automatic cleanup doesn't yet extend to taskManager's own tracked tasks — a component's own timers are always cleaned up, but a task it started via taskManager.run(...) keeps running/queued after that component is destroyed unless you cancel it yourself. See task-manager's own "Not (yet) supported" list.

Reference implementation — apps/timers-demo

Every mechanism on this page has a router-mounted, scaled chapter in apps/timers-demo — 3 chapters (/basic-lifecycle through /focus-teardown-ordering), reachable with REWIND/FAST-FORWARD once compiled and sideloaded. Chapter 1 covers the one-shot/recurring basics, including cancelling a pending setTimeout before it ever fires; chapter 2 proves the automatic unmount cleanup through a genuinely two-component-level-deep nested cascade and an {#each} list-item removal; chapter 3 proves the ordering between focus-recovery and the ft_unmount cascade for a synchronous (non-animated) teardown of a currently-focused, ticking widget. See findings/timers-demo-app.md for what each chapter covers and findings/demo-app-conventions.md for the app-structure convention it follows.

⚠️ Not (yet) supported

  • ○ setTimeout/setInterval may appear only as a bare statement, or the entire right-hand side of a plain <local> = assignment — never nested inside a larger expression, a condition, or a loop header. clearTimeout/clearInterval may appear only as a bare statement.
  • ○ None of the four may be used inside a derived expression, a dynamic template {expr} binding, or an {#if}/{#each} condition/collection/key expression — all recompute repeatedly, which would leak (or try to clear) a new Timer node every time.
  • ○ Not supported inside a .flsh class body — same reasons taskManager.onAlertChanged/onResult aren't.
  • ○ The names setTimeout/setInterval/clearTimeout/clearInterval are reserved — they cannot be used as a field/derived/state/function/parameter name.

Exact grammar: GRAMMAR.md. Full feature status: docs/features.md.