Error Handling
When Things Go Wrong at Runtime
Imagine you're playing a video game and your character tries to open a door that doesn't exist. The game has two choices:
- Crash -- the screen goes black, game over.
- Handle it -- show a message like "That door is locked" and let you keep playing.
Error handling is how your program does option 2. Instead of crashing when something unexpected happens, you catch the problem and decide what to do about it.
See also: Error Messages explains the different error types that Pebble can produce and how error messages are formatted.
Try / Catch
Pebble uses try and catch blocks to handle errors:
Here's what happens:
- Pebble runs the code inside
try { }. - If something goes wrong (like dividing by zero), Pebble stops the try block immediately.
- It jumps to the
catchblock and puts the error message into the variablee. - Your program keeps running after the catch block -- no crash!
If nothing goes wrong, the catch block is skipped entirely.
Catching Without a Variable
If you don't care what the error was, you can leave out the variable:
Throw
You can create your own errors with throw:
fn check_age(age) {
if age < 0 {
throw "Age cannot be negative"
}
return age
}
try {
check_age(-5)
} catch e {
print(e)
}
Output: Age cannot be negative
You can throw any value -- strings, numbers, even arrays:
If a throw happens and there's no try/catch around it, your program
crashes with an error -- just like dividing by zero would.
Finally
Sometimes you need code that runs no matter what -- whether the try
block succeeded or an error was caught. That's what finally is for:
try {
print("Trying something risky...")
throw "oops"
} catch e {
print("Caught: {e}")
} finally {
print("This always runs!")
}
Output:
The finally block runs on every way out of the try/catch:
- when the try block finishes normally,
- after a catch block handles an error,
- when a
returnleaves the try or catch block (the finally runs before the function actually returns), - when a
breakorcontinuejumps out of atryinside a loop, - and even when the catch block itself
throws a new error (the finally runs before that error bubbles up).
That's what makes it perfect for cleanup tasks -- closing a file or releasing a resource -- because you can trust it to run no matter how the block exits.
fn read_first(xs) {
try {
return xs[0]
} catch e {
return -1
} finally {
print("cleanup always happens")
}
}
Calling read_first([10]) prints cleanup always happens and then returns
10 -- the finally runs even though the try block used return.
Catching Built-in Errors
Pebble's built-in errors (like dividing by zero or accessing an array
index that doesn't exist) can all be caught with try/catch:
Output: Oops: Index 99 out of bounds for list of length 3
Errors Bubble Up
If a function throws an error (or triggers a built-in error), and that
function doesn't have its own try/catch, the error bubbles up to
whoever called it:
fn step_one() {
throw "broken"
}
fn step_two() {
step_one()
}
try {
step_two()
} catch e {
print(e)
}
Output: broken
The error passed through step_two and step_one until it found a
try/catch that could handle it. Think of it like a ball bouncing up
through layers until someone catches it.
Practical Patterns
Safe Division
fn safe_div(a, b) {
try {
return a / b
} catch e {
return 0
}
}
print(safe_div(10, 2))
print(safe_div(10, 0))
Output:
Safe Array Access
fn get_or_default(xs, index, fallback) {
try {
return xs[index]
} catch e {
return fallback
}
}
let colours = ["red", "green", "blue"]
print(get_or_default(colours, 1, "unknown"))
print(get_or_default(colours, 99, "unknown"))
Output:
How It Works Under the Hood
When the compiler sees a try block, it emits three special bytecode
instructions:
| Opcode | What It Does |
|---|---|
SETUP_TRY |
Push a handler onto the exception stack -- "if something goes wrong, jump here" |
POP_TRY |
Remove the handler -- the try block finished normally |
THROW |
Pop a value from the stack and trigger an exception |
When an exception happens (either from throw or a built-in error like
division by zero), the VM:
- Looks at the exception handler stack for the nearest handler.
- Unwinds the call stack and value stack back to where the handler was set up.
- Pushes the error value onto the stack (so
catch ecan bind it). - Jumps to the catch block's first instruction.
If there's no handler on the stack, the exception becomes a runtime error and the program stops -- with a stack trace showing the chain of function calls that led to the error.
See also: Stack Traces explains how Pebble shows the "breadcrumb trail" of function calls when an error isn't caught.
Summary
| Feature | Syntax | Purpose |
|---|---|---|
| Try | try { ... } |
Run code that might fail |
| Catch | catch e { ... } |
Handle the error (variable is optional) |
| Finally | finally { ... } |
Run cleanup code no matter what |
| Throw | throw value |
Create your own error |