Skip to main content

Strict Mode Deep Dive

Learning Objectives

  • Understand the three type checking modes in Luau
  • Know that Rive scripts run in strict mode by default
  • Recognize and fix common type errors caught by strict mode
  • Use strict mode effectively in your development workflow

AE/JS Syntax Comparison

If you're coming from After Effects expressions or JavaScript, understanding type checking modes is crucial:

ConceptJavaScript/AELuau (Rive)Notes
Type checkingNone (runtime only)--!strict (compile time)Luau catches errors BEFORE running
Untyped modeDefault--!nocheckBoth skip type checking
Strict modeN/A--!strictRecommended for Rive
TypeScripttsc --strict--!strictSimilar concept
Empty array[] works{} needs typelocal t: {string} = {}
Missing propertyRuntime errorCompile errorStrict catches early
Critical Difference

JavaScript: Bugs crash at runtime, sometimes only in edge cases

// JavaScript - crashes when health is undefined
let result = player.health - damage; // No error until runtime!

Luau Strict: Bugs caught immediately during development

-- Luau --!strict - error shown in editor BEFORE running
local result = player.health - damage -- Error: health might be nil

This is WHY strict mode is strongly recommended—your animations must work reliably everywhere, and there's no debugger when they run on users' devices.


Rive Context

Rive scripts run in strict mode by default for type safety. This means all the benefits below are enabled automatically:

  1. Type safety at compile time: Errors are caught before your animation runs
  2. Better editor integration: Autocomplete knows exact types
  3. Reliable runtime behavior: No surprise nil values or type mismatches
  4. Cleaner code: Forces you to think about data flow

Why does this matter? When your animation ships to users, there's no debugger. Strict mode ensures your animation works correctly because all type errors were caught during development.

When you write a Node Script, the Rive compiler expects proper type annotations for your export type, function parameters, and return values. Missing or incorrect types will prevent your script from compiling.


The Three Type Checking Modes

Luau supports three modes, controlled by a directive at the top of your file:

--!strict    -- Full type checking (RECOMMENDED for Rive)
--!nonstrict -- Partial checking, allows 'any' inference
--!nocheck -- No type checking

Mode Comparison

Feature--!nocheck--!nonstrict--!strict
Type inferenceNoPartialFull
Errors on type mismatchNoSomeYes
Requires annotationsNoSometimesYes (for functions)
Used in RiveNoNoYes

Practice Exercises

Exercise 1: Parse Text Safely ⭐

Premise

Strict mode refuses to mix text and numbers in math. When data arrives as strings, you must convert it explicitly.

Goal

By the end of this exercise, you will convert text to numbers before arithmetic.

Use Case

A UI field or designer Input provides a string value, but you need numeric math for animation. Parsing explicitly keeps strict mode happy and avoids hidden coercion bugs.

Example scenarios:

  • Parsing user-entered values
  • Converting serialized data

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise1_ParseText
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise1 = {}

function init(self: Exercise1): boolean
local maxHealth = 100
local damageText: string = "25"
local damage = 0

-- TODO: Convert damageText into a number and store in damage

local remaining = maxHealth - damage
print(`ANSWER: remaining={remaining}`)
return true
end

function draw(self: Exercise1, renderer: Renderer)
end

return function(): Node<Exercise1>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Use tonumber to convert damageText
  2. If conversion fails, keep damage as 0

Expected Output

Your console output should display the remaining health after converting the string damage to a number and subtracting it.


Verify Your Answer

Verify Your Answer

Checklist

  • No string math remains
  • damage is a number
  • Output matches the expected line

Exercise 2: Optional Label ⭐

Premise

Optional values are common in strict mode. You must check for nil before using them.

Goal

By the end of this exercise, you will handle an optional string safely.

Use Case

A label may or may not be provided by a designer. You need a safe fallback so the UI always has text.

Example scenarios:

  • Optional text overrides
  • Missing configuration values

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise2_OptionalLabel
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise2 = {}

function init(self: Exercise2): boolean
local hint: string? = nil
local label = ""

-- TODO: Use hint when present, otherwise use "Default"

print(`ANSWER: label={label}`)
return true
end

function draw(self: Exercise2, renderer: Renderer)
end

return function(): Node<Exercise2>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Check if hint is nil
  2. Use "Default" when hint is missing

Expected Output

Your console output should display the label value — either the hint (if provided) or the fallback default string.


Verify Your Answer

Verify Your Answer

Checklist

  • Optional value is checked before use
  • label is always a string
  • Output matches the expected line

Exercise 3: Return Paths ⭐

Premise

Strict mode requires every code Path to return a value that matches the function signature. Clear boolean helpers avoid hidden nil returns.

Goal

By the end of this exercise, you will return a boolean on all paths.

Use Case

You are flagging a critical state for a health indicator. The helper must always return true or false.

Example scenarios:

  • Critical health checks
  • Toggle guards

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise3_ReturnPaths
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise3 = {}

local function isCritical(hp: number): boolean
-- TODO: Return true when hp <= 10, otherwise false
return false
end

function init(self: Exercise3): boolean
local critical = isCritical(8)
print(`ANSWER: critical={critical}`)
return true
end

function draw(self: Exercise3, renderer: Renderer)
end

return function(): Node<Exercise3>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Return true when hp is 10 or less
  2. Return false otherwise

Expected Output

Your console output should display whether the health value is at or below the critical threshold (returns true or false).


Verify Your Answer

Verify Your Answer

Checklist

  • Function returns on all paths
  • Output matches the expected line
  • No nil returns

Exercise 4: Literal Union Mode ⭐

Premise

Literal unions limit a variable to known values. This keeps State Machine values predictable and typo-proof.

Goal

By the end of this exercise, you will use a literal union type.

Use Case

You are switching between animation modes. Using a literal union prevents invalid strings from slipping in.

Example scenarios:

  • State machine modes
  • Named animation states

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise4_LiteralUnion
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise4 = {}

type Mode = "idle" | "run" | "hit"

function init(self: Exercise4): boolean
local mode: Mode = "idle"
-- TODO: Set mode to "run"

print(`ANSWER: mode={mode}`)
return true
end

function draw(self: Exercise4, renderer: Renderer)
end

return function(): Node<Exercise4>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Set the mode to "run"
  2. Keep the mode typed as Mode

Expected Output

Your console output should display the current mode value from the literal union type.


Verify Your Answer

Verify Your Answer

Checklist

  • Mode uses the literal union type
  • Value is one of the allowed literals
  • Output matches the expected line

Exercise 5: Table Shape Enforcement ⭐⭐

Premise

Strict mode enforces table shapes. If a table is typed, every field must match its definition.

Goal

By the end of this exercise, you will work with a typed table and valid fields.

Use Case

You are storing stats for a UI meter. Correct field types ensure your calculations never receive unexpected values.

Example scenarios:

  • Typed stats tables
  • Parameter packs for helpers

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise5_TableShape
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise5 = {}

type Stats = {
hp: number,
mp: number,
}

function init(self: Exercise5): boolean
-- TODO: Set hp to 80 and mp to 20
local stats: Stats = {
hp = 0,
mp = 0,
}

local ratio = stats.hp / stats.mp
print(`ANSWER: ratio={ratio}`)
return true
end

function draw(self: Exercise5, renderer: Renderer)
end

return function(): Node<Exercise5>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Set hp to 80 and mp to 20
  2. Keep the table typed as Stats

Expected Output

Your console output should display the ratio computed from the typed stats table (hp divided by mp).


Verify Your Answer

Verify Your Answer

Checklist

  • Table fields match the Stats type
  • Ratio equals 4
  • Output matches the expected line

Exercise 6: Input Safety ⭐⭐

Premise

Input<T> values are intended to be read-only. While not runtime-enforced, you should derive writable state from inputs rather than reassigning them.

Goal

By the end of this exercise, you will compute a derived value from an input.

Use Case

A designer sets a speed input. Your script computes an adjusted value used for animation timing.

Example scenarios:

  • Speed multipliers
  • Difficulty scaling

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise6_InputSafety
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise6 = {
speed: Input<number>,
adjusted: number,
}

function init(self: Exercise6): boolean
-- TODO: Compute adjusted as speed * 1.2
self.adjusted = 0

print(`ANSWER: adjusted={self.adjusted}`)
return true
end

function draw(self: Exercise6, renderer: Renderer)
end

return function(): Node<Exercise6>
return {
init = init,
draw = draw,
speed = 2.5,
adjusted = late(),
}
end

Assignment

Complete these tasks:

  1. Read speed from the input
  2. Store speed * 1.2 in adjusted

Expected Output

Your console output should display the adjusted value computed by multiplying the speed input by the factor.


Verify Your Answer

Verify Your Answer

Checklist

  • Input is read-only
  • adjusted is computed in init
  • Output matches the expected line

Knowledge Check

Q:What directive enables full type checking in Luau scripts?
Q:What happens with 'local items = {}' in strict mode?
Q:Which of these is NOT caught by strict mode?
Q:Why does Rive use strict mode by default?

Common Mistakes

Common Issues
  1. Strict mode is default: Rive scripts run in strict mode by default. You can add --!strict explicitly for clarity, but it's not required.

    -- Strict mode is already enabled by default in Rive
    -- Adding --!strict is optional but makes intent clear

    export type MyScript = {}
  2. Empty tables without types: local t: {string} = {} not local t = {}

  3. Plain tables for Rive types: Use Vector.xy() not {x = 100, y = 50}

  4. Not handling optionals: Always check string? before using as string

    -- WRONG
    print(user.email:upper())

    -- CORRECT
    if user.email then
    print(user.email:upper())
    end
  5. Missing function return types: Top-level functions need explicit return type annotations

    -- WRONG
    local function add(a: number, b: number)
    return a + b
    end

    -- CORRECT
    local function add(a: number, b: number): number
    return a + b
    end

Self-Assessment Checklist

  • I understand that Rive scripts run in strict mode by default
  • I can fix "cannot infer type of empty table" errors
  • I know the difference between strict, nonstrict, and nocheck modes
  • I can properly annotate function signatures
  • I understand how strict mode narrows types through control flow
  • I use Rive's constructors (Vector.xy, Color.rgba) correctly

Next Steps