Error handling basics
Quadrate has a robust error handling system built around fallible functions.
Fallible functions
A fallible function is one that might fail. Mark it with ! after the signature:
fn divide(a:i64 b:i64 -- result:i64)! {
// This function can fail
}
The ! tells the compiler this function can return an error.
Signaling panics
Use the panic instruction to signal an error:
fn divide(a:i64 b:i64 -- result:i64)! {
dup 0 == if {
drop2
"division by zero" -1 panic
}
/
}
The panic instruction takes (in push order):
- An error message (string)
- An error code (integer)
Handling errors
When you call a fallible function, you must handle the error with if:
fn main() {
10 2 divide if {
// Success: result is on stack
"Result: " print print nl
} else {
// Error: stack is unchanged from before the call
"Division failed!" print nl
}
}
The compiler enforces this - you must either handle the error with if/else, or use ! to abort on error (see below).
How it works
After calling a fallible function:
- Success (if branch): The function's outputs are on the stack
- Error (else branch): The function's outputs are NOT on the stack (the stack is as it was before the call)
Complete example
fn divide(a:i64 b:i64 -- result:i64)! {
dup 0 == if {
drop2
"division by zero" -1 panic
}
/
}
fn main() {
// This will fail
1 0 divide if {
"1 / 0 = " print print nl
} else {
"Error: Cannot divide by zero!" print nl
}
// This will succeed
10 2 divide if {
"10 / 2 = " print print nl
} else {
"Unexpected error" print nl
}
}
Output:
Error: Cannot divide by zero!
10 / 2 = 5
Skipping error checks
Call a fallible function with ! to skip error handling:
fn divide_and_double(a:i64 b:i64 -- result:i64)! {
divide! // Aborts if divide fails
2 *
}
Warning: If divide fails, the program will panic. Only use ! when you're certain the call won't fail.
Stack behavior
When a fallible function succeeds, the if branch has the function's outputs on the stack.
When a fallible function fails (via panic), the else branch does NOT have the function's outputs - the stack is as it was before the call.
fn get_two( -- a:i64 b:i64)! {
"failed" 1 panic
}
fn main() {
get_two if {
// Success: outputs are on stack
-> b -> a
a print nl
b print nl
} else {
// Error: outputs are NOT on stack
"Function failed" print nl
}
}
Standard library errors
Many standard library functions are fallible:
use str
use io
fn main() {
// String operations
"hello" 1 3 str::substring if {
print nl // "ell"
} else {
"substring failed" print nl
}
// File operations
"test.txt" io::Read io::open if {
-> file
"File opened" print nl
file io::close
} else {
"Could not open file" print nl
}
}
Retrieving error information
Use the err instruction to retrieve the error message and code:
fn divide(a:i64 b:i64 -- result:i64)! {
dup 0 == if {
drop2
"division by zero" 42 panic
}
/
}
fn main() {
10 0 divide if {
print nl
} else {
err -> code -> msg
"Error: " print msg print " (code " print code print ")" print nl
}
}
Output:
Error: division by zero (code 42)
The err instruction pushes (-- msg code):
- msg: The error message string
- code: The error code integer (on top)
This is useful with switch to handle different error codes:
fn main() {
some_operation if {
// Success
} else {
err switch {
1 {
drop "File not found" print nl
}
2 {
drop "Permission denied" print nl
}
_ {
"Unknown error: " print print nl
}
}
}
}
Key rules
- Mark fallible functions with
!after the signature - Use
"message" code panicto signal panics - Handle errors with
if { success } else { error } - Use
errin the else branch to retrieve error details - The compiler enforces error handling
- The
ifbranch has the function outputs; theelsebranch does not
What's next?
Learn Error Handling Patterns for common scenarios.