🧮

Statements & expressions
if/else, ternary, ==/!=/</>/<=/>=, !, chain safety, loops, try/catch, anonymous functions.

Function bodies are ordinary BrightScript-flavored text — assignments, return, print, calls all pass through as-is — with a handful of JS-shaped control-flow forms layered on top that the compiler expands into real BrightScript at compile time.

if / else if / else

Always parens around the condition, always { } for a block — no then/end if in source. An inline, braceless single-statement form is also allowed.

if (condition) {
  ...
} else if (condition2) {
  ...
} else {
  ...
}

if (condition) doOneThing()   ' inline form — no braces, runs to end of line

Ternary ? :

BrightScript has no ternary operator — this lowers to a hoisted temp variable plus an ordinary if/else. Fully nestable and chainable, but only as the entire right-hand side of a plain assignment or a state write — not inside a derived default, a template binding, or an if condition.

FavoriteCounter.thr (excerpt)
value = cond1 ? (cond2 ? a : b) : c
state tierLabel = newCount < 3 ? "starting out" : newCount < 10 ? "collector" : "super fan"

Crash-safe comparison == / !=

A bare BrightScript =/<> crashes at runtime on a type mismatch (e.g. comparing an integer field to Invalid). ==/!= are DSL-only sugar over a shared ft_equals(...) helper: numbers compare by value across subtypes (3 == 3.0 is true), arrays/assocarrays/SceneGraph nodes compare by reference identity (JS-==-style), and a genuine type mismatch returns false instead of crashing.

if (count == 1) {
  return "1 favorite"
}
' == / != are crash-safe (ft_equals under the hood) — a bare BrightScript = / <>
' throws at runtime when the two operands are different, incompatible types.

Crash-safe relational operators < / > / <= / >=

Same idea as ==/!=, but there's no obviously-correct fallback value for an incompatible ordering comparison the way false is for equality — so instead of returning a guessed answer, a genuine mismatch throws. </>/<=/>= are DSL-only sugar over a shared ft_relationalGuard(...) helper: both operands must be numbers (any subtype) or both strings, or it throws a structured {code, message} — catchable with an ordinary try/catch. Existing source needs no changes; every </>/<=/>= is guarded automatically.

derived isWide: boolean = width > 300

try {
  isOverBudget = spent >= budget
} catch (e) {
  ' e.code == "relational/type-mismatch" when spent/budget aren't both
  ' numbers (or both strings) — e.message has the human-readable detail.
  isOverBudget = false
}
' < / > / <= / >= are crash-safe too (ft_relationalGuard under the hood) —
' a genuine type mismatch THROWS instead of guessing a fallback value, since
' unlike ==/!=, there's no obviously-correct answer for "is X greater than Y?"
' when X and Y aren't comparable. Catchable via try/catch, same as any throw.

Crash-safe NOT !

A bare BrightScript Not crashes at runtime when its operand isn't a real Boolean (e.g. Invalid, or a numeric field nobody guarded first). ! is DSL-only sugar over a shared ft_not(...) helper: it checks the operand's type first, negating only a genuine Boolean and returning false instead of crashing on anything else. Fully nestable — !!x, !(a == b), and so on.

FavoriteCounter.thr (excerpt)
if (!showCelebration) {
  return
}
state showCelebration = !showCelebration   ' toggles a boolean field/state safely

' ! is crash-safe (ft_not under the hood) — a bare BrightScript Not throws at
' runtime when the operand isn't a real Boolean. Fully nestable: !!x, !(a == b).

Chain safety ?. / ?[ / ?(

Every member access, array/index access, and function call in generated .brs is automatically rewritten to BrightScript's own native optional-chaining operators, so a chain never crashes just because something in the middle turns out to be Invalid. You never write ?./?[/?( yourself — the compiler inserts it everywhere, automatically, and doing so by hand in source is a compile error. Unlike ==/! above, there's no runtime helper involved — it's a pure syntactic rewrite using Roku's own built-in operators (OS 11.0+).

Three spots are left exactly as written, because Roku's own operators can't legally appear there: an assignment's target (a.b.c = x stays plain), a bare statement whose entire content is a discarded call (obj.foo.bar() alone on its own line — though that call's own arguments are still a normal read and still get chained), and a call whose callee is a bare identifier — a plain global function or built-in, never ?( in any context, including a read: someFunction(a, b) stays exactly that. ?( only ever appears when the callee is itself a chain (obj.method() → obj?.method?()).

derived userName: string = profile.account.name
derived badgeLabel: string = describeBadge(pendingBadge)  ' bare call, still a read

private function refresh() {
  cache.tracker.recordVisit()          ' a bare statement — left untouched
  logEvent(cache.tracker.summarize())  ' its own argument still gets chained
  cache.counters.total = 0             ' an assignment target — left untouched
}

' generated .brs:
'   m.userName = m?.top?.profile?.account?.name
'   m.badgeLabel = private_describeBadge(m?.top?.pendingBadge)  ' bare callee never gets ?(
'   m.top.cache.tracker.recordVisit()
'   private_logEvent(m?.top?.cache?.tracker?.summarize?())      ' chained callee does
'   m.top.cache.counters.total = 0

Loops

for/for each/while — JS-bracket sugar over BrightScript's own loop forms. No inline (braceless) form for any of the three, unlike if.

ScheduleList.thr (excerpt)
for (i = 0 to days.Count() - 1) {
  day = days[i]
  scale y = i * 40
  day.y = y
}

for each (day in schedule) {
  print day.title
}

while (retries < 3) {
  retries = retries + 1
}

try / catch

try {
  result = riskyCall()
} catch (e) {
  print "failed: "; e.message
}

Anonymous function expressions

BrightScript's own array interface has no Map/Filter/ForEach — but Function is a real first-class value, so a hand-written helper plus an anonymous function argument gets you the same result:

ScheduleList.thr (excerpt)
private function filterDays(days: object, predicate: Function): object {
  updated = []
  for (i = 0 to days.Count() - 1) {
    if (predicate(days[i])) {
      updated.Push(days[i])
    }
  }
  return updated
}

public function removeToday() {
  updated = filterDays(schedule, function (day: object): boolean {
    return NOT day.isToday
  })
  state schedule = updated
}

Raw BrightScript passthrough

An escape hatch for a BrightScript idiom this DSL has no sugar for — everything between ' flash-theater:raw and ' flash-theater:end-raw is copied into the generated .brs completely unchanged: no identifier-rewrite (write the real generated form by hand — m.top.<field>, m.<derived>, private_<fn>()), no elision, only re-indentation to match the surrounding code. Valid as a statement inside a function/method/constructor body, or as a top-level <script> declaration — the second form's content lands at the very end of the generated init(), after everything else has already run. The content is still validated as real BrightScript at compile time, so a genuine syntax error inside one is caught immediately, attributed to you rather than reported as a compiler bug.

private function describeLimit(): string {
  ' flash-theater:raw
  result = "limit is " + CreateObject("roDeviceInfo").GetModel()
  ' flash-theater:end-raw
  return result
}

' flash-theater:raw
m.top.limit = m.top.limit + 1
' flash-theater:end-raw

Reference implementation — apps/statements-demo

Every mechanism on this page has a router-mounted, scaled chapter in apps/statements-demo — 4 chapters (/conditionals through /anonymous-functions-and-raw), reachable with REWIND/ FAST-FORWARD once compiled and sideloaded. Each chapter shows a default, no-customization example alongside a deliberately different, customized one — e.g. ConditionalsDemo.thr's block-form if/else if/else chain (default) next to a NESTED ternary driving a state write from two independent booleans (customized), and SafeOperatorsDemo.thr's ordinary ==/!=/! usage (default) next to a deliberately-triggered ft_relationalGuard throw, caught with try/catch (customized). This is the live, compiling reference for the whole page — see findings/statements-demo-app.md for what each chapter covers and findings/demo-app-conventions.md for the app-structure convention it follows.

⚠️ Not (yet) supported

  • ○ Real BrightScript for i = 0 to 10 ... end for is not usable anymore — for is a claimed DSL keyword now, always bracketed.
  • ○ No finally, and catch is mandatory — there's no catch-less try.
  • ○ An anonymous function cannot close over the enclosing function's own local variables (matches real BrightScript semantics) — only field/derived/state/function references and m still resolve inside its body.
  • ○ An anonymous function directly inside a template attribute/bind:/on:key/{#if}/{#each} expression — write a named function and call that instead.
  • ○ A raw BrightScript passthrough block cannot nest, cannot be the single statement of an inline if/else, cannot appear inside a template/binding expression, and has no top-level form inside a .flsh class body (only inside an existing method/constructor) — a class isn't guaranteed to have any lifecycle sub to run one from.

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