Debugging Rive Scripts
Master the tools and techniques for finding and fixing issues in your Rive scripts.
Rive Context
Rive scripts run inside the Rive runtime, not in a traditional development environment. Debugging relies on:
- Console Panel - View
print()output and runtime messages - Problems Tab - See pre-execution errors (type mismatches, syntax errors)
- Strict Mode - Catch type errors before runtime
- Systematic Logging - Strategic print statements to trace execution
Script Panels
Rive provides four panels for script development: Console, Problems, Changes, and Testing.
Console
Shows all runtime output during playback:
print()statements from your scripts- Runtime error messages
- Script lifecycle events
Features:
- Copy button - Copy console contents to clipboard
- Clear button - Clear console output
- Fullscreen - Expand for better visibility
Open via View > Console or the panel menu to see print() output from your scripts.
Problems
Shows pre-execution errors that are detected before your script runs:
- Type mismatches - When types don't match (e.g., assigning string to number)
- Syntax errors - Typos, missing keywords, invalid Luau syntax
- Binding errors - Missing or incorrect input/output bindings
Features:
- Click an error to jump to the affected line in your script
- Hover for explanations and suggested fixes
- Errors update in real-time as you type
- Problems: Shows errors detected by the type checker BEFORE running (like TypeScript)
- Console: Shows errors that occur DURING runtime (like JavaScript console)
Check Problems first — if there are errors there, fix them before running your script.
The print() Function
print() is your primary debugging tool. Output appears in Rive's Console panel.
--!strict
export type DebugDemo = {
counter: number,
}
function init(self: DebugDemo): boolean
print("Script initialized")
print("Counter starts at:", self.counter)
return true
end
function advance(self: DebugDemo, seconds: number): boolean
self.counter += 1
-- Only print occasionally to avoid flooding the console
if self.counter % 60 == 0 then
print(`Tick: {self.counter}, Elapsed: {self.counter / 60:.1f}s`)
end
return true
end
function draw(self: DebugDemo, renderer: Renderer)
end
return function(): Node<DebugDemo>
return {
init = init,
advance = advance,
draw = draw,
counter = 0,
}
end
Expected Console Output:
Script initialized
Counter starts at: 0
Tick: 60, Elapsed: 1.0s
Tick: 120, Elapsed: 2.0s
Common Errors and Fixes
Use these patterns to diagnose the most frequent Rive scripting errors.
1. attempt to index nil with 'x'
Cause: A late() value was never assigned in the editor, or you are accessing a Node that doesn't exist.
Fix:
- Assign the Input in the Inspector (for
Input<T>andlate()). - Guard access if it can be optional:
if self.target then
self.target.opacity = 1
end
2. Type 'number' does not have key 'value'
Cause: You treated a script Input<number> like a ViewModel property.
Fix:
- Use
self.someInputdirectly (it is the number). - Use
context:viewModel()only for ViewModel properties.
-- Input<T> (read-only value)
local speed = self.speed
-- ViewModel (has .value)
local vm = context:viewModel()
local speedProp = vm and vm:getNumber("speed")
if speedProp then
speedProp.value = speedProp.value + 1
end
3. attempt to call a nil value
Cause: You forgot to return a lifecycle function from the factory.
Fix: Make sure the returned table includes the function:
return function(): Node<MyType>
return {
init = init,
draw = draw,
advance = advance,
}
end
4. Type errors in Paint.with(...) or Vector.xy(...)
Cause: Wrong types for fields (e.g., string instead of Color, number instead of Vector).
Fix: Match the API signature exactly:
local paint = Paint.with({ style = "stroke", thickness = 2, color = Color.rgb(255, 0, 0) })
local pos = Vector.xy(100, 50)
Exercise 1: Tracing Execution Flow ⭐
Premise
Understanding when lifecycle functions are called is essential for debugging. init runs once, update runs when inputs change, advance runs every frame.
By the end of this exercise, you will be able to Add print statements to trace when each lifecycle function is called, including the frame count in advance.
Use Case
This pattern shows up whenever you build behavior in Rive scripts.
Example scenarios:
- Debugging script behavior
- Driving animation logic
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise1_Exercise1TracingExecutionFlow
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Lifecycle tracer to understand when functions are called
export type LifecycleTracer = {
name: Input<string>,
frameCount: number,
path: Path,
paint: Paint,
}
function init(self: LifecycleTracer): boolean
-- TODO 1: Print that init was called, include self.name
-- Example: print(`[{self.name}] init called`)
self.frameCount = 0
self.path = Path.new()
self.paint = Paint.with({ style = "fill", color = Color.rgb(100, 150, 200) })
return true
end
function update(self: LifecycleTracer)
print(`[{self.name}] update called - input changed`)
end
function advance(self: LifecycleTracer, seconds: number): boolean
self.frameCount += 1
-- TODO 2: Print advance info for first 3 frames only
-- Include: self.name, self.frameCount, seconds (formatted to 4 decimals)
-- Example format: [Tracer] advance #1, dt=0.0167
if self.frameCount == 3 then
print("ANSWER: lifecycle")
end
return true
end
function draw(self: LifecycleTracer, renderer: Renderer)
-- Note: Avoid printing in draw - it runs frequently on repaint!
self.path:reset()
self.path:moveTo(Vector.xy(-30, -30))
self.path:lineTo(Vector.xy(30, -30))
self.path:lineTo(Vector.xy(30, 30))
self.path:lineTo(Vector.xy(-30, 30))
self.path:close()
renderer:drawPath(self.path, self.paint)
end
return function(): Node<LifecycleTracer>
return {
init = init,
update = update,
advance = advance,
draw = draw,
name = "Tracer",
frameCount = 0,
path = Path.new(),
paint = Paint.new(),
}
end
Assignment
Complete these tasks:
- Add print statements to trace when each lifecycle function is called, including the frame count in advance.
- Run the script and verify the console output
- Copy the
ANSWER:line into the validator
Expected Output
Console prints the relevant debug lines for this exercise.
ANSWER: <your result>
Verify Your Answer
Checklist
-
--!strictis at the top - All TODOs are replaced with working code
- Console output includes the
ANSWER:line
Key points:
- Print in
initto confirm initialization - Limit prints in
advanceto avoid console spam - Never print in
draw- it runs frequently on repaint
Exercise 2: Debugging State Changes ⭐⭐
Premise
Tracking state changes over time helps identify issues like values not updating, updating incorrectly, or oscillating unexpectedly. Helper functions ensure consistent formatting.
By the end of this exercise, you will be able to Complete the logState helper function and add calls to track state in init and during significant movement.
Use Case
This pattern shows up whenever you build behavior in Rive scripts.
Example scenarios:
- Debugging script behavior
- Driving animation logic
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise2_Exercise2DebuggingStateChanges
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- State debugger to track how values change over time
export type StateDebugger = {
targetX: Input<number>,
currentX: number,
velocity: number,
moveCount: number,
path: Path,
paint: Paint,
}
-- TODO 1: Complete the logState helper function
local function logState(label: string, self: StateDebugger)
-- Print format: [{label}] target={targetX}, current={currentX:.2f}, vel={velocity:.2f}
-- TODO: Use print with string interpolation
end
function init(self: StateDebugger): boolean
self.currentX = 0
self.velocity = 0
self.moveCount = 0
self.path = Path.new()
self.paint = Paint.with({ style = "fill", color = Color.rgb(80, 180, 120) })
-- TODO 2: Call logState with "INIT" label
return true
end
function update(self: StateDebugger)
logState("INPUT CHANGED", self)
end
function advance(self: StateDebugger, seconds: number): boolean
local oldX = self.currentX
-- Spring physics
local displacement = self.targetX - self.currentX
local springForce = displacement * 100
local dampingForce = self.velocity * 10
self.velocity += (springForce - dampingForce) * seconds
self.currentX += self.velocity * seconds
-- TODO 3: Log significant changes only (when deltaX > 1)
local deltaX = math.abs(self.currentX - oldX)
-- If deltaX > 1, call logState with "MOVING" label and increment moveCount
if self.moveCount == 2 then
print("ANSWER: statetrack")
end
-- Rebuild path
self.path:reset()
local size = 20
self.path:moveTo(Vector.xy(self.currentX - size, -size))
self.path:lineTo(Vector.xy(self.currentX + size, -size))
self.path:lineTo(Vector.xy(self.currentX + size, size))
self.path:lineTo(Vector.xy(self.currentX - size, size))
self.path:close()
return true
end
function draw(self: StateDebugger, renderer: Renderer)
renderer:drawPath(self.path, self.paint)
end
return function(): Node<StateDebugger>
return {
init = init,
update = update,
advance = advance,
draw = draw,
targetX = 100,
currentX = 0,
velocity = 0,
moveCount = 0,
path = Path.new(),
paint = Paint.new(),
}
end
Assignment
Complete these tasks:
- Complete the logState helper function and add calls to track state in init and during significant movement.
- Run the script and verify the console output
- Copy the
ANSWER:line into the validator
Expected Output
Console prints the relevant debug lines for this exercise.
ANSWER: <your result>
Verify Your Answer
Checklist
-
--!strictis at the top - All TODOs are replaced with working code
- Console output includes the
ANSWER:line
Key points:
- Use a helper function (
logState) for consistent formatting - Add labels to identify where the log came from
- Only log significant changes to avoid console spam
Common Errors and Fixes
Error: Script Not Appearing in Menu
Symptom: Your script does not show up when trying to add it to a node.
Cause: Missing draw function or incorrect return type.
-- BAD: Missing draw function
return function(): Node<MyType>
return {
init = init,
-- No draw!
}
end
-- GOOD: Include draw even if empty
return function(): Node<MyType>
return {
init = init,
draw = draw,
}
end
Error: "attempt to index nil"
Symptom: Runtime error when accessing a property.
Cause: Field not initialized or wrong name.
-- BAD: Accessing uninitialized field
function init(self: MyNode): boolean
self.path:reset() -- self.path is nil!
return true
end
-- GOOD: Initialize first
function init(self: MyNode): boolean
self.path = Path.new() -- Create it
self.path:reset() -- Now safe
return true
end
Error: Type Mismatch
Symptom: Strict mode error about incompatible types.
Cause: Wrong type annotation or assignment.
-- BAD: Type mismatch
export type MyNode = {
count: number,
}
function init(self: MyNode): boolean
self.count = "hello" -- Error: string vs number
return true
end
-- GOOD: Correct type
function init(self: MyNode): boolean
self.count = 0 -- Correct: number
return true
end
Error: "Unknown global"
Symptom: Strict mode error about undefined variable.
Cause: Typo in variable name or missing declaration.
-- BAD: Typo in variable name
function advance(self: MyNode, seconds: number): boolean
self.elasped += seconds -- Typo: "elasped" vs "elapsed"
return true
end
-- GOOD: Correct spelling
function advance(self: MyNode, seconds: number): boolean
self.elapsed += seconds -- Correct
return true
end
Exercise 3: Debugging a Broken Script ⭐⭐
Premise
Common bugs include uninitialized fields (nil errors), typos in property names, and accessing undefined fields. Strict mode catches many of these at edit time.
By the end of this exercise, you will be able to Find and fix the three bugs in this script: missing initialization, wrong property name, and incorrect field access.
Use Case
This pattern shows up whenever you build behavior in Rive scripts.
Example scenarios:
- Debugging script behavior
- Driving animation logic
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise3_Exercise3DebuggingABrokenScript
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- This script has 3 bugs - find and fix them!
export type BuggyCircle = {
radius: Input<number>,
path: Path,
paint: Paint,
}
function init(self: BuggyCircle): boolean
-- BUG 1: self.path methods are called but path may not be initialized
-- TODO: Add self.path = Path.new() before using it
self.path:reset()
local r = self.radius
self.path:moveTo(Vector.xy(-r, 0))
self.path:quadTo(Vector.xy(0, -r), Vector.xy(r, 0))
self.path:quadTo(Vector.xy(0, r), Vector.xy(-r, 0))
self.path:close()
-- BUG 2: "colour" is not a field in our type - should be "paint"
-- TODO: Fix the property name and use Paint.with({...})
self.colour = Color.rgb(255, 100, 100)
print(`Circle initialized with radius: {r}`)
return true
end
function advance(self: BuggyCircle, seconds: number): boolean
-- BUG 3: "rad" is not a field - should be "radius"
-- TODO: Fix the field name
local r = self.rad
if r == self.radius then
print("Radius check passed")
print("All bugs fixed!")
print("ANSWER: debugged")
end
return true
end
function draw(self: BuggyCircle, renderer: Renderer)
renderer:drawPath(self.path, self.paint)
end
return function(): Node<BuggyCircle>
return {
init = init,
advance = advance,
draw = draw,
radius = 50,
path = Path.new(),
paint = Paint.new(),
}
end
Assignment
Complete these tasks:
- Find and fix the three bugs in this script: missing initialization, wrong property name, and incorrect field access.
- Run the script and verify the console output
- Copy the
ANSWER:line into the validator
Expected Output
Console prints the relevant debug lines for this exercise.
ANSWER: <your result>
Verify Your Answer
Checklist
-
--!strictis at the top - All TODOs are replaced with working code
- Console output includes the
ANSWER:line
Strict Mode Errors
Strict mode catches errors at edit time. Here are common ones:
Unknown Global
--!strict
function init(self: MyNode): boolean
prnt("hello") -- Error: Unknown global 'prnt'
print("hello") -- Correct
return true
end
Missing Type Annotation
--!strict
-- Error: Missing type annotation
function helper(x)
return x * 2
end
-- Correct: With types
function helper(x: number): number
return x * 2
end
Incompatible Assignment
--!strict
export type Counter = {
count: number,
}
function init(self: Counter): boolean
self.count = nil -- Error: nil incompatible with number
self.count = 0 -- Correct
return true
end
Debugging Workflow
Step 1: Identify the Symptom
- Script not appearing? Check for
drawfunction. - Runtime error? Check the error message for line number.
- Wrong behavior? Add print statements.
Step 2: Add Strategic Logging
function advance(self: MyNode, seconds: number): boolean
print("advance START")
print(" seconds:", seconds)
print(" self.value:", self.value)
-- Your logic here
print("advance END, result:", self.value)
return true
end
Step 3: Isolate the Problem
Comment out sections until the error disappears:
function init(self: MyNode): boolean
print("Step 1")
self.path = Path.new()
print("Step 2")
-- self.path:moveTo(Vector.xy(0, 0)) -- Comment out
print("Step 3")
-- self.path:close() -- Comment out
return true
end
Step 4: Fix and Verify
After fixing, remove debug prints or guard them:
local DEBUG = false
function advance(self: MyNode, seconds: number): boolean
if DEBUG then
print("advance:", seconds)
end
return true
end
Performance Debugging
Detecting Slow Code
--!strict
export type PerfTest = {
frameCount: number,
totalTime: number,
}
function init(self: PerfTest): boolean
self.frameCount = 0
self.totalTime = 0
return true
end
function advance(self: PerfTest, seconds: number): boolean
self.frameCount += 1
self.totalTime += seconds
-- Report average frame time every 60 frames
if self.frameCount % 60 == 0 then
local avgMs = (self.totalTime / self.frameCount) * 1000
print(`Average frame time: {avgMs:.2f}ms`)
end
return true
end
function draw(self: PerfTest, renderer: Renderer)
end
return function(): Node<PerfTest>
return {
init = init,
advance = advance,
draw = draw,
frameCount = 0,
totalTime = 0,
}
end
Key Takeaways
- Use
print()strategically - too much output hides important messages - Add labels to identify where logs come from
- Use strict mode to catch type errors early
- Initialize all fields before using them
- Comment out code to isolate problems
- Remove debug prints before finalizing
- Set Debug Level to "Full" during development for detailed error messages (see Script Compilation Settings)
Next Steps
- Continue to Resources
- Need a refresher? Review Quick Reference