Skip to content

Failures

On a Result, ? unwraps Ok on success, and force-returns Err on failure:

fn read_config(path: string) -> Result<Config, error> {
let bytes = os.ReadFile(path)?
on success, bytes is the value inside Ok
on failure, read_config returns Err
parse_config(bytes)
}

Equivalent without ?:

fn read_config(path: string) -> Result<Config, error> {
let bytes = match os.ReadFile(path) {
Ok(contents) => contents,
Err(failure) => return Err(failure),
}
parse_config(bytes)
}

On an Option, ? unwraps Some when present, and force-returns None when absent:

fn get_name(id: int) -> Option<string> {
let user = users.get(id)?
when present, user is the value inside Some
when absent, get_name returns None
Some(user.name)
}

Equivalent without ?:

fn get_name(id: int) -> Option<string> {
let user = match users.get(id) {
Some(found) => found,
None => return None,
}
Some(user.name)
}

? propagates an error unchanged. To record where it passed through, wrap_err() prepends a message to the error while keeping the original as the cause:

fn read_config(path: string) -> Result<Config, error> {
let bytes = os.ReadFile(path).wrap_err("reading config file")?
parse_config(bytes)
}

An Ok passes through untouched. If os.ReadFile fails, ? propagates the wrapped error, which reads reading config file: open /etc/app.conf: no such file or directory. The original is preserved, so errors.Is and errors.As still match against it.

A try block groups fallible calls and evaluates to a Result. Inside it, ? returns from the block rather than from the function:

fn load_config() -> Config {
let result = try {
let path = env.get("CONFIG_PATH")?failure propagates to result
let file = fs.read(path)?
parse_toml(file)?
}
match result {
Ok(config) => config,
Err(_) => Config.default(),
}
}

Any type with an Error() method can be returned as an error:

struct ValidationError {
field: string,
message: string,
}
impl ValidationError {
fn Error(self) -> string {
f"{self.field}: {self.message}"
}
}
fn validate(input: Input) -> Result<Input, ValidationError> {
if input.name == "" {
return Err(ValidationError { field: "name", message: "required" })
}
Ok(input)
}

It then propagates with ? into any function returning error:

fn process(input: Input) -> Result<Input, error> {
let valid = validate(input)?ValidationError satisfies error
Ok(valid)
}

Where a Go function returns a value alongside an error, the result is a Partial<T, E>. Handle its three cases with match:

match reader.Read(buf) {
Partial.Ok(n) => process(buf[..n]),
Partial.Err(err) => return Err(err),
Partial.Both(n, err) => {read bytes, then reached EOF
process(buf[..n])
if errors.Is(err, io.EOF) { return Ok(()) }
return Err(err)
},
}

An error is an interface value, so == is refused on it. Compare with errors.Is, which also matches an error wrapped by wrap_err().

Lisette omits Rust’s unwrap(). To unwrap a value, use:

To catch panics at runtime, Go uses recover() in a deferred anonymous function:

go func() {
defer func() {
if r := recover(); r != nil {
log.Println(r)
}
}()
handleConnection(conn)
}()
recover.go

Lisette’s recover block serves the same purpose, yielding a PanicValue that carries what the panic passed:

let result = recover {
handle_connection(conn)
}
if let Err(pv) = result {
log.Println(pv.message())
}
recover.lis