Error handling
Oxie uses Rust’s error model — errors are values of type Result<T, E> — with Kotlin/Swift
style syntax on top. There are no exceptions and no hidden costs: everything below compiles to
ordinary Result code.
throws, throw and ?
Section titled “throws, throw and ?”A function that can fail declares throws:
fun parse_age(text: &str): u32 throws { val age: u32 = text.trim().parse()? // `?` passes a failure on to the caller if (age > 150) throw "age $age is not realistic" age // wrapped in Ok automatically}
fun main() { println("${parse_age("42"):?}") println("${parse_age("abc"):?}") println("${parse_age("200"):?}")}pub(crate) fn parse_age(text: &str) -> ::std::result::Result<u32, crate::__oxie::AnyError> { let age: u32 = text.trim().parse()?; if age > 150 { return Err(format!("age {age} is not realistic").into()); } Ok(age)}
fn main() { println!("{:?}", parse_age("42")); println!("{:?}", parse_age("abc")); println!("{:?}", parse_age("200"));}Ok(42)Err(ParseIntError { kind: InvalidDigit })Err("age 200 is not realistic")| Oxie | Rust |
|---|---|
fun f(): T throws |
fn f() -> Result<T, AnyError> |
fun f(): T throws MyError |
fn f() -> Result<T, MyError> |
fun f() throws |
fn f() -> Result<(), AnyError> |
return x / last expression x |
return Ok(x) / Ok(x) |
throw e |
return Err(e.into()) |
expr? |
expr? |
AnyError is Box<dyn std::error::Error + Send + Sync>: any error type converts into it,
and so do strings, so throw "message" and throw "value $x is bad" work.
Calling another Oxie function that throws from a throwing function propagates its error
automatically when you use the call as a statement or return value — like an exception would:
fun check(x: i32) throws { if (x < 0) throw "negative: $x"}
fun check_all(xs: &[i32]) throws { for (x in xs) check(*x) // same as `check(*x)?`}
fun main() { println("${check_all(&[1, 2]):?}") println("${check_all(&[1, -2]):?}")}pub(crate) fn check(x: i32) -> ::std::result::Result<(), crate::__oxie::AnyError> { if x < 0 { return Err(format!("negative: {x}").into()); } Ok(())}
pub(crate) fn check_all(xs: &[i32]) -> ::std::result::Result<(), crate::__oxie::AnyError> { for x in xs { check(*x)?; } Ok(())}
fn main() { println!("{:?}", check_all(&[1, 2])); println!("{:?}", check_all(&[1, -2]));}Ok(())Err("negative: -2")When you assign the result instead (val r = check(5)), you get the Result to inspect.
Error types
Section titled “Error types”error enum declares an error type with a message per case. Oxie implements Display,
std::error::Error and Debug for it:
import std::num::ParseIntError
error enum ConfigError { Missing(key: String) = "missing setting '$key'" OutOfRange(key: String, value: i64, max: i64) = "$key = $value is larger than $max" BadNumber(ParseIntError) // wraps another error}
fun read_port(text: &str): u16 throws ConfigError { if (text.is_empty()) throw ConfigError.Missing("port") val n: i64 = text.parse()? // ParseIntError converts via From if (n > 65535) throw ConfigError.OutOfRange("port", n, 65535) n as u16}
fun main() { for (input in listOf("8080", "", "x", "99999")) { when (val r = read_port(input)) { Ok(port) -> println("port $port") Err(e) -> println("error: $e") } }}use std::num::ParseIntError;
#[derive(Debug)]pub(crate) enum ConfigError { Missing { key: String }, OutOfRange { key: String, value: i64, max: i64 }, BadNumber(ParseIntError),}
impl ::std::fmt::Display for ConfigError { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { match self { Self::Missing { key } => write!(f, "missing setting '{key}'"), Self::OutOfRange { key, value, max } => write!(f, "{key} = {value} is larger than {max}"), Self::BadNumber(__0) => ::std::fmt::Display::fmt(__0, f), } }}
impl ::std::error::Error for ConfigError { fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> { match self { Self::BadNumber(e) => Some(e), #[allow(unreachable_patterns)] _ => None, } }}
impl ::std::convert::From<ParseIntError> for ConfigError { fn from(e: ParseIntError) -> Self { Self::BadNumber(e) }}
pub(crate) fn read_port(text: &str) -> ::std::result::Result<u16, ConfigError> { if text.is_empty() { return Err(ConfigError::Missing { key: String::from("port") }.into()); } let n: i64 = text.parse()?; if n > 65535 { return Err(ConfigError::OutOfRange { key: String::from("port"), value: n, max: 65535 }.into()); } Ok(n as u16)}
fn main() { for input in vec!["8080", "", "x", "99999"] { { let r = read_port(&input); match r { Ok(port) => { println!("port {port}"); } Err(e) => { println!("error: {e}"); } } } }}port 8080error: missing setting 'port'error: invalid digit found in stringerror: port = 99999 is larger than 65535= "template"is the message;$fieldrefers to the case’s fields and$0to positional ones.- A case with a single field whose type is an error (its name ends in
Error) gets aFromconversion, so?turns that error into this case, andsource()returns it. Mark a case@fromto get the conversion for other types. - A case without a message shows its wrapped error, or its name in words (
NotFound→ “not found”).
A single error type can be a class:
error class TooBig(value: i64, limit: i64) = "$value exceeds the limit of $limit"
fun check(v: i64): i64 throws TooBig { if (v > 100) throw TooBig(v, 100) v}
fun main() { println("${check(5):?}") println("${check(500).unwrap_err()}")}#[derive(Debug)]pub(crate) struct TooBig { pub(crate) value: i64, pub(crate) limit: i64,}
impl TooBig { #[allow(dead_code)] pub(crate) fn new(value: i64, limit: i64) -> Self { Self { value, limit, } }}
impl ::std::fmt::Display for TooBig { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { write!(f, "{} exceeds the limit of {}", self.value, self.limit) }}
impl ::std::error::Error for TooBig {}
pub(crate) fn check(v: i64) -> ::std::result::Result<i64, TooBig> { if v > 100 { return Err(TooBig::new(v, 100).into()); } Ok(v)}
fn main() { println!("{:?}", check(5)); println!("{}", check(500).unwrap_err());}Ok(5)500 exceeds the limit of 100try / catch
Section titled “try / catch”try { } runs a block in which ? and throw jump to catch instead of leaving the
function:
fun main() { val total = try { val a: i32 = "10".parse()? val b: i32 = "x".parse()? a + b } catch (e) { println("could not add: $e") 0 } println("total = $total")}fn main() { let total = match '__oxie_try1: { let a: i32 = match "10".parse() { Ok(__oxie_v2) => __oxie_v2, Err(__oxie_e3) => break '__oxie_try1 Err(::std::convert::From::from(__oxie_e3)) }; let b: i32 = match "x".parse() { Ok(__oxie_v4) => __oxie_v4, Err(__oxie_e5) => break '__oxie_try1 Err(::std::convert::From::from(__oxie_e5)) }; Ok::<_, crate::__oxie::AnyError>(a + b) } { Ok(__oxie_v6) => __oxie_v6, Err(e) => { println!("could not add: {e}"); 0 } }; println!("total = {total}");}could not add: invalid digit found in stringtotal = 0catch (e)receives anAnyError. Writecatch (e: MyError)to receive a specific type (errors are converted withFrom).try { }withoutcatchevaluates to aResult:val r = try { a()? + b()? }.- It compiles to a Rust labeled block, so
return,breakandcontinueinside it behave normally.
Defaults: ?:
Section titled “Defaults: ?:”?: gives a value when a Result is an error (or an Option is None):
fun main() { val n = "abc".parse::<i32>() ?: 0 val m = "7".parse::<i32>() ?: 0 println("$n $m")}fn main() { let n = crate::__oxie::ElvisOr::or_val("abc".parse::<i32>(), 0); let m = crate::__oxie::ElvisOr::or_val("7".parse::<i32>(), 0); println!("{n} {m}");}0 7The right side can also leave the function: val user = find(id) ?: throw "user $id not found"
or ?: return.
Adding context
Section titled “Adding context”.context("...") wraps an error (or a missing Option value) with a message, keeping the
original as its cause:
import std::fs
fun load(path: &str): String throws { val text = fs.read_to_string(path).context("cannot read config '$path'")? text}
fun main() { when (val r = load("/no/such/file.toml")) { Ok(t) -> println(t) Err(e) -> { println("error: $e") println("cause: ${e.source().unwrap()}") } }}use std::fs;
pub(crate) fn load(path: &str) -> ::std::result::Result<String, crate::__oxie::AnyError> { let text = fs::read_to_string(path).context(format!("cannot read config '{path}'"))?; Ok(text)}
fn main() { { let r = load("/no/such/file.toml"); match r { Ok(t) => { println!("{}", t); } Err(e) => { println!("error: {e}"); println!("cause: {}", e.source().unwrap()); } } }}error: cannot read config '/no/such/file.toml'cause: No such file or directory (os error 2).with_context { "..." } builds the message only when there is an error.
main() throws
Section titled “main() throws”A throwing main prints the error and its causes and exits with status 1:
import std::fs
fun main() throws { val config = fs.read_to_string("/no/such/app.toml").context("loading app.toml")? println(config)}// exits with an error:// error: loading app.toml// caused by: No such file or directory (os error 2)use std::fs;
fn __oxie_main() -> ::std::result::Result<(), crate::__oxie::AnyError> { let config = fs::read_to_string("/no/such/app.toml").context("loading app.toml")?; println!("{}", config); Ok(())}
fn main() { if let Err(err) = __oxie_main() { crate::__oxie::report(err); ::std::process::exit(1); }}Unrecoverable errors
Section titled “Unrecoverable errors”panic("message"), unreachable(), todo() and !! (unwrap) stop the program as in Rust.
Use them for bugs, not for expected failures. Under oxie run and oxie test the panic
message names the .oxi line (panicked at src/main.oxi:12:5).
Using Rust error crates
Section titled “Using Rust error crates”Everything interoperates with the Rust ecosystem: thiserror and anyhow work as usual
(@derive(thiserror::Error), throws anyhow::Error), and an Oxie error enum is a normal Rust
error type that Rust code can match on and convert.