Error Handling
Error handling in Axe is explicit through use of error unions from returns. It is similar to Go in this regard in that there is no try/catch construct.
Creating an error can be done through use of the std.errors module.
use std.errors;
use std.io;
def main() {
panic(error.create("This is some runtime error."));
println "Will never print...";
}
In this case we create an error through use of the create static function on the error type. The panic function is used to stop the program in the case of a fatal error. Though generally it is preferred to set the return type of your function to an error union, such that error propagation is made possible. For example:
model SomeFunctionResult {
result: union {
err: error;
result: string;
}
}
pub def some_function(): SomeFunctionResult {
}
Axe provides a straightforward error handling model using return values and the error type, combined with the panic function for unrecoverable errors. This approach ensures predictable, efficient error handling without exceptions or try-catch blocks.
The Error Type
The error type represents an error condition with an associated message:
use std.errors (error, panic, enforce);
val my_error: error = error.create("Something went wrong");
Creating Errors
Use error.create() to create a new error with a message:
use std.errors (error);
def validate_age(age: i32): bool {
if age < 0 {
val err: error = error.create("Age cannot be negative");
panic(err);
}
return age >= 18;
}
Return Value Based Error Handling
The idiomatic Axe approach uses return values to signal success or failure:
use std.string;
use std.io (println_str);
/// Parse an integer, returning success/failure
def parse_int(text: string): i32 {
// Return 0 for invalid input, or parsed value for valid
if str_len(text) == 0 {
return 0; // Sentinel value indicating failure
}
mut result: i32 = 0;
for mut i = 0; i < str_len(text); i++ {
val ch: char = get_char(text, i);
if ch >= '0' and ch <= '9' {
result = result * 10 + (ch - '0');
} else {
return 0; // Invalid character
}
}
return result;
}
// Usage with error checking
def main() {
val num: i32 = parse_int(str("42"));
if num == 0 {
println_str str("Parse failed");
return;
}
println_str str("Successfully parsed number");
}
Using Models for Complex Results
For operations that need to return both a value and status information, use models:
use std.string;
use std.errors (error, panic);
/// Result model combining success status and value
model ParseResult {
success: bool;
value: i32;
error_msg: string;
}
def try_parse_int(text: string): ParseResult {
mut result: ParseResult;
result.success = false;
result.value = 0;
result.error_msg = str("");
if str_len(text) == 0 {
result.error_msg = str("Input is empty");
return result;
}
mut value: i32 = 0;
for mut i = 0; i < str_len(text); i++ {
val ch: char = get_char(text, i);
if ch >= '0' and ch <= '9' {
value = value * 10 + (ch - '0');
} else {
result.error_msg = str("Invalid character at position ");
return result;
}
}
result.success = true;
result.value = value;
return result;
}
// Usage
def main() {
val result: ParseResult = try_parse_int(str("42"));
if result.success {
println_str str("Parsed successfully");
} else {
println_str result.error_msg;
}
}
Guard Clauses for Early Returns
Guard clauses enable clean error handling by checking conditions upfront:
use std.string;
use std.io (println_str);
def process_file(filename: string, content: string): bool {
// Guard: validate inputs
if str_len(filename) == 0 {
println_str str("error: filename cannot be empty");
return false;
}
if str_len(content) == 0 {
println_str str("error: content cannot be empty");
return false;
}
if str_len(content) > 1000000 {
println_str str("error: content too large");
return false;
}
// Main logic - all guards passed
do_write_file(filename, content);
return true;
}
Benefits of Guard Clauses
- Fail Fast: Check for error conditions immediately
- Reduced Nesting: Avoid deeply nested conditional blocks
- Clarity: Error cases are explicit and visible
- Maintainability: Easy to add or remove error checks
Panic for Unrecoverable Errors
Use panic() for errors from which recovery is impossible. This should be reserved for true exceptional conditions:
use std.errors (error, panic);
use std.arena (Arena);
def allocate_resources(size: usize): Arena {
if size == 0 {
val err: error = error.create("Cannot allocate zero bytes");
panic(err); // Program will terminate
}
mut arena: Arena = Arena.create(size);
return arena;
}
When to Use Panic:
- Out of memory conditions
- Corrupted internal state
- Invariant violations that indicate program bugs
- Unsafe operation failures
When NOT to Use Panic:
- Expected failures (invalid user input, network issues)
- Recoverable errors (file not found, parsing failures)
- Conditions that should propagate to the caller
Enforce for Assertions
The enforce() function combines a condition check with panic for assertion-like behavior:
use std.errors (error, enforce);
def divide(a: i32, b: i32): i32 {
enforce(b != 0, error.create("Division by zero"));
return a / b;
}
def validate_index(index: i32, size: i32) {
enforce(index >= 0, error.create("Index cannot be negative"));
enforce(index < size, error.create("Index out of bounds"));
}
Enforce vs. Panic
enforce()is more concise for simple assertionspanic()is more explicit and allows complex logic- Both terminate the program on failure
Error Propagation Patterns
Pattern 1: Sentinel Values
Use special return values to indicate errors:
def find_in_list(items: ref StringList, target: string): i32 {
for mut i = 0; i < len(deref(items)); i++ {
if equals_c(StringList.get(items, i), target) {
return i;
}
}
return -1; // Not found sentinel
}
// Usage
val index: i32 = find_in_list(my_list, search_term);
if index < 0 {
println_str str("Item not found");
}
Pattern 2: Boolean Success Flag
def write_config(filename: string, data: string): bool {
if !validate_config(data) {
return false; // Validation failed
}
if !create_backup(filename) {
return false; // Backup creation failed
}
if !write_file(filename, data) {
return false; // File write failed
}
return true; // Success
}
// Usage
if !write_config(str("config.txt"), config_data) {
println_str str("Failed to write configuration");
return;
}
Pattern 3: Result Model
For detailed error information, use a model containing status and details:
model FileResult {
success: bool;
data: string;
error_code: i32;
error_msg: string;
}
def read_file(path: string): FileResult {
mut result: FileResult;
result.success = false;
result.data = str("");
result.error_code = 0;
result.error_msg = str("");
// Attempt to read file
if !file_exists(path) {
result.error_code = 1;
result.error_msg = str("File not found");
return result;
}
result.data = read_file_contents(path);
result.success = true;
return result;
}
Cleanup and Resource Management
Always clean up resources, even when errors occur. Use guard clauses to structure cleanup:
use std.arena (Arena);
use std.io (println_str);
def process_data(filename: string): bool {
// Allocate resources
mut arena: Arena = Arena.create(1024 * 1024);
// Guard: file validation
if !file_exists(filename) {
println_str str("File not found");
Arena.destroy(addr(arena));
return false;
}
// Guard: read validation
val content: string = read_file(filename);
if str_len(content) == 0 {
println_str str("File is empty");
Arena.destroy(addr(arena));
return false;
}
// Process data
val result: bool = do_processing(addr(arena), content);
// Cleanup (always executed)
Arena.destroy(addr(arena));
return result;
}
Common Error Handling Patterns
Validating Multiple Conditions
def create_user(name: string, email: string, age: i32): bool {
// Validate all inputs before processing
if str_len(name) == 0 {
println_str str("Name cannot be empty");
return false;
}
if str_len(email) == 0 or find_char(email, '@') < 0 {
println_str str("Invalid email");
return false;
}
if age < 18 {
println_str str("User must be 18 or older");
return false;
}
// All validations passed
store_user(name, email, age);
return true;
}
Chaining Operations with Error Checks
def process_pipeline(input: string): bool {
// Step 1
val step1_result: string = validate_input(input);
if str_len(step1_result) == 0 {
println_str str("Validation failed");
return false;
}
// Step 2
if !transform_data(step1_result) {
println_str str("Transformation failed");
return false;
}
// Step 3
if !store_output(step1_result) {
println_str str("Storage failed");
return false;
}
return true;
}
Error Handling Best Practices
1. Use Appropriate Error Reporting
// Good: Clear, actionable error messages
if age < 0 {
println_str str("error: age cannot be negative");
return false;
}
// Poor: Vague error message
if age < 0 {
println_str str("Invalid input");
return false;
}
2. Validate Early
// Good: Validate at function entry
def process(data: string): bool {
if str_len(data) == 0 {
return false;
}
// ... rest of implementation
}
// Poor: Let invalid data propagate
def process(data: string): bool {
val result: i32 = do_something(data); // May fail
// ...
}
3. Document Error Conditions
/// Process a file
///
/// Returns true if successful, false if:
/// - File does not exist
/// - File is unreadable
/// - Content is invalid
/// - Insufficient memory
def process_file(path: string): bool {
// implementation
}
4. Use Models for Complex Scenarios
// Good: Rich error information
model ApiResponse {
success: bool;
status_code: i32;
data: string;
error_msg: string;
}
// Poor: Limited information
def call_api(url: string): i32 {
// only returns status code
}
5. Be Consistent in Error Handling
// Good: Consistent pattern throughout
def operation_a(): bool { /* ... */ }
def operation_b(): bool { /* ... */ }
def operation_c(): bool { /* ... */ }
// Poor: Inconsistent approaches
def operation_a(): bool { /* ... */ }
def operation_b(): i32 { /* ... */ }
def operation_c(): Model { /* ... */ }
Comparison with Other Languages
vs. Exceptions (Java, Python, C++)
Axe (Return Values): Java (Try-Catch):
├─ Explicit error handling ├─ Exception propagation
├─ No control flow exceptions ├─ Implicit exceptions
├─ Predictable performance ├─ Exception handling overhead
└─ Clear error paths └─ Multiple exception types
Axe advantages:
- No hidden control flow
- Deterministic performance
- Simpler to reason about
- No unexpected jumps
Java advantages:
- Less boilerplate for common cases
- Rich exception hierarchy
vs. Go (error interface)
Axe: Go:
├─ Named return models ├─ Multiple return values
├─ Bool/model-based errors ├─ error interface
├─ Guard clauses ├─ if err != nil
└─ Type-safe results └─ Dynamic error handling
Both similar in philosophy but Axe is more type-safe.