Skip to main content

Error Handling

Oxide provides Swift-style error handling with throws, try, and do-catch blocks.

Throwing Functions

Mark functions that can fail with throws:

fn parseNumber(s: String) throws: Int {
if s.isEmpty() {
throw "Empty string"
}
s.parse().unwrap()
}

fn readConfig(path: String) throws: Config {
let content = try readFile(path)
try parseConfig(content)
}

Transpiles to:

fn parse_number(s: &str) -> Result<isize, Box<dyn std::error::Error>> {
if s.is_empty() {
return Err("Empty string".into());
}
Ok(s.parse().unwrap())
}

fn read_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
let content = read_file(path)?;
parse_config(&content)
}

The throw Statement

Throw errors from throwing functions:

fn validateAge(age: Int) throws {
if age < 0 {
throw "Age cannot be negative"
}
if age > 150 {
throw "Age seems unrealistic"
}
}

fn divide(a: Int, b: Int) throws: Int {
if b == 0 {
throw "Division by zero"
}
a / b
}

Try Variants

try - Propagate Errors

Use in throwing functions or do-catch blocks:

fn processFile(path: String) throws: Data {
let content = try readFile(path) // Propagates error
let parsed = try parseData(content) // Propagates error
parsed
}

try? - Convert to Optional

Returns null on error:

let content = try? readFile("config.txt")  // String?

if let data = try? parseJson(input) {
process(data)
} else {
useDefault()
}

// With null coalesce
let config = try? loadConfig() ?? Config.default()

Transpiles to:

let content = read_file("config.txt").ok();

if let Some(data) = parse_json(&input).ok() {
process(data);
} else {
use_default();
}

try! - Force Unwrap

Panics on error (use sparingly):

let content = try! readFile("required.txt")  // Panics if error

Transpiles to:

let content = read_file("required.txt").unwrap();
warning

Only use try! when you're certain the operation will succeed, such as in tests or when failure indicates a bug.

Do-Catch Blocks

Handle errors with Swift-style do-catch:

do {
let config = try loadConfig()
let data = try fetchData(config.url)
process(data)
} catch {
println!("Error: $error")
}

Transpiles to:

match (|| -> Result<_, Box<dyn std::error::Error>> {
let config = load_config()?;
let data = fetch_data(&config.url)?;
process(data);
Ok(())
})() {
Ok(_) => {},
Err(error) => {
println!("Error: {}", error);
}
}

Catching Specific Error Types

do {
let data = try fetchData(url)
} catch NetworkError {
println!("Network error: $error.code")
} catch ParseError {
println!("Parse error: $error.message")
} catch {
println!("Unknown error: $error")
}

Transpiles to:

match fetch_data(&url) {
Ok(data) => { /* ... */ },
Err(error) => {
if let Some(e) = error.downcast_ref::<NetworkError>() {
println!("Network error: {}", e.code);
} else if let Some(e) = error.downcast_ref::<ParseError>() {
println!("Parse error: {}", e.message);
} else {
println!("Unknown error: {}", error);
}
}
}

The Implicit error Variable

In catch blocks, error is automatically available:

do {
try riskyOperation()
} catch {
// 'error' is available here
log(error)
return defaultValue
}

Combining with Optionals

Converting Result to Optional

// try? converts Result<T, E> to T?
let value: Int? = try? parseNumber(input)

// Chain with safe operators
let doubled = (try? parseNumber(input))?.mapped { it * 2 } ?? 0

Error or Null

Handle both missing values and errors:

fn getUser(id: Int) throws: User? {
if id <= 0 {
throw "Invalid ID"
}
database.findUser(id) // Returns User?
}

// Handle all cases
do {
if let user = try getUser(id) {
welcome(user)
} else {
println!("User not found")
}
} catch {
println!("Error: $error")
}

Custom Error Types

Define your own error types:

@[derive(Debug)]
enum ApiError {
case network(message: String)
case parse(line: Int, column: Int)
case notFound
}

fn fetchApi(url: String) throws: Response {
let response = try? http::get(url)
guard let r = response else {
throw ApiError.network(message = "Failed to connect")
}

if r.status == 404 {
throw ApiError.notFound
}

r
}

Result Type

For more explicit error handling, use Result directly:

fn divide(a: Int, b: Int): Result<Int, String> {
if b == 0 {
return Err("Division by zero")
}
Ok(a / b)
}

// Pattern match on result
match divide(10, 2) {
Ok(value) -> println!("Result: $value")
Err(e) -> println!("Error: $e")
}

// Use Result extension methods
let result = divide(10, 2)
.mapped { it * 2 }
.unwrapOr(0)

See Standard Library - Result for Result extension methods.

Best Practices

  1. Use throws for recoverable errors - Network failures, parse errors, etc.
  2. Use try? with defaults - try? loadConfig() ?? defaultConfig
  3. Reserve try! for guarantees - When failure is a bug
  4. Catch specific errors first - More specific catches before general
// Good: Specific error handling
do {
let data = try fetchData()
} catch NetworkError {
retryWithBackoff()
} catch {
logError(error)
}

// Good: Optional with default
let config = try? loadConfig() ?? Config.default()

// Avoid: Ignoring errors completely
// let _ = try? dangerousOperation()

See Also