Error Handling
GX has no exceptions. Errors are handled with return values — usually a bool that indicates success, combined with an out parameter for the result. This matches how C and Zig handle errors: explicit, visible, and zero-cost.
The Basic Pattern
Return bool for success/failure, write the result through out:
fn safe_divide:bool(a:f32, b:f32, out result:f32) {
if (b == 0.0) {
return false
}
*result = a / b
return true
}
fn main() {
var r:f32 = 0.0
if (safe_divide(10.0, 3.0, &r)) {
print("10 / 3 = {r}\n")
}
if (!safe_divide(5.0, 0.0, &r)) {
print("error: division by zero\n")
}
}
The caller must check the return value before using the result. Ignoring errors takes explicit effort.
Error Enum for Multiple Failure Modes
When there’s more than one way things can go wrong, return an enum:
enum ParseError {
Ok
EmptyInput
InvalidChar
Overflow
}
fn parse_digit:ParseError(ch:char, out value:i32) {
if (ch == '0') { *value = 0; return ParseError.Ok }
if (ch == '1') { *value = 1; return ParseError.Ok }
if (ch == '2') { *value = 2; return ParseError.Ok }
if (ch == '3') { *value = 3; return ParseError.Ok }
if (ch == '4') { *value = 4; return ParseError.Ok }
if (ch == '5') { *value = 5; return ParseError.Ok }
if (ch == '6') { *value = 6; return ParseError.Ok }
if (ch == '7') { *value = 7; return ParseError.Ok }
if (ch == '8') { *value = 8; return ParseError.Ok }
if (ch == '9') { *value = 9; return ParseError.Ok }
return ParseError.InvalidChar
}
fn main() {
var v:i32 = 0
var err = parse_digit('7', &v)
if (err == ParseError.Ok) {
print("parsed: {v}\n")
} else {
print("parse failed\n")
}
}
The caller checks against specific enum values to distinguish failure modes.
Result Struct Pattern
For richer error info, use a struct that holds both the value and the error state:
struct ParseResult {
value:i32
ok:bool
message:str
}
fn parse_int:ParseResult(s:str) {
var r:ParseResult
r.value = 0
r.ok = true
r.message = ""
if (s.len == 0) {
r.ok = false
r.message = "empty input"
return r
}
// Simplified: assume single digit
if (s.len == 1) {
var ch = s[0]
if (ch >= '0') {
if (ch <= '9') {
r.value = ch - '0'
return r
}
}
}
r.ok = false
r.message = "not a digit"
return r
}
fn main() {
var r = parse_int("7")
if (r.ok) {
print("value: {r.value}\n")
} else {
print("error: {r.message}\n")
}
}
Early Return on Error
A common pattern is the “guard clause” — check errors at the top and return early:
fn process_user:bool(name:str, age:i32, out greeting:str) {
// Guard: validate inputs
if (name.len == 0) { return false }
if (age < 0) { return false }
if (age > 150) { return false }
// All checks passed — do the actual work
*greeting = "Hello, " + name
return true
}
fn main() {
var msg:str = ""
if (process_user("Alice", 30, &msg)) {
print("{msg}\n")
}
if (!process_user("", 25, &msg)) {
print("error: invalid name\n")
}
if (!process_user("Bob", 200, &msg)) {
print("error: invalid age\n")
}
}
Defer for Cleanup on Error Paths
Combine defer with the error pattern so cleanup always runs, even on early return:
fn load_data:bool(out result:i32) {
var arr:array<i32>
arr.init()
defer { arr.free() } // runs on any exit
arr.push(10)
arr.push(20)
arr.push(30)
if (arr.len() == 0) {
return false // defer still runs
}
*result = arr.at(0) + arr.at(1) + arr.at(2)
return true
}
fn main() {
var sum:i32 = 0
if (load_data(&sum)) {
print("sum = {sum}\n")
}
}
Checking Pointers for Null
GX uses 0 as the null pointer value. Check before dereferencing:
fn find_user:*i32(users:*i32, count:i32, target:i32) {
// In GX, you can't return a pointer — this is just for illustration.
// Real code uses out params.
return 0
}
Since functions can’t return pointers, you typically check pointers passed in as parameters:
struct Node {
value:i32
next:*Node
}
fn print_list(head:*Node) {
var cur = head
while (cur != 0) {
print("{cur.value} ")
cur = cur.next
}
print("\n")
}
Panics for Unrecoverable Errors
For bugs that should never happen in correct code, use array bounds checks or explicit aborts. In debug builds (-O0), GX inserts bounds checks:
fn main() {
var arr:i32[3] = {1, 2, 3}
// var x = arr[10] // panic in debug: "index 10 out of bounds (len 3)"
print("done\n")
}
Panics terminate the program with a message and location — they’re for programmer errors, not user input errors. User input errors should use the return-value pattern.
Try it — Write a function
safe_sqrt:bool(x:f32, out result:f32)that returns false for negative inputs. Test it in the Playground.
Expert Corner
Why no exceptions: Exceptions have two problems for systems programming:
- Hidden control flow: Any function call might throw. Every caller must consider cleanup for every line. The actual flow of execution isn’t visible in the source.
- Runtime overhead: Stack unwinding tables, try/catch blocks, possible heap allocations for exception objects. In C++ with
-fno-exceptionsor Rust withpanic = "abort", the cost is real enough that games disable them.
GX’s return-value approach makes error paths explicit, zero-overhead, and visible at every call site. You can tell by reading the code which functions can fail.
Why bool + out param instead of a Result type: GX doesn’t have generic types (yet), so there’s no Result<T, E>. The equivalent is fn foo:bool(..., out result:T) — slightly more verbose but compiles to the same machine code as Rust’s Result::Ok branch. For multi-way errors, use a result struct or an error enum.
Compare to Go: Go’s idiom is value, err := foo() with multiple return values. GX’s idiom is if (foo(&value)) { ... }. Same information, different shape. Go puts the error last; GX puts the success bool as the return value. Go’s pattern relies on tuple returns, which GX doesn’t have — out params fill that gap.
Compare to Rust: Rust’s Result<T, E> + ? operator is more ergonomic but requires generics, pattern matching, and implicit propagation. GX takes the simpler path: manual checks, explicit error enums, no language features to learn. The tradeoff is more boilerplate on error paths, but complete visibility of control flow.
When to use panic vs return false: Use panic for:
- Array bounds violations
- Null pointer dereference
- Invariant violations (“this should never happen”)
- Programming bugs
Use return false for:
- Failed parsing of user input
- Network timeouts
- File not found
- Division by zero from runtime data
- Any error that could happen in a correct program
The rule: panics are bugs, return values are recoverable errors. If the caller has a reasonable action to take, use a return value. If the only sane response is “stop the program and fix the code,” use panic.