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 lineTernary ? :
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.
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.
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 = 0Loops
for/for each/while —
JS-bracket sugar over BrightScript's own loop forms. No inline (braceless) form for any of the three, unlike
if.
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:
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-rawReference 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 foris not usable anymore —foris a claimed DSL keyword now, always bracketed. - ○ No
finally, andcatchis mandatory — there's no catch-lesstry. - ○ An anonymous function cannot close over the enclosing function's own local variables (matches real BrightScript semantics) — only
field/derived/state/function references andmstill 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.flshclass 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.