Errors: Result, Option & Exceptions
The same fallible task in all seven languages: parse a string to an integer and compute 100 / n, where two distinct things can go wrong — the string isn't a number, and the number is zero. Watch the fundamental split: do errors travel in the type (a Result/Either value you must destructure, with ?-style early-return) or out-of-band as exceptions you try/catch? Notice how guji, Rust, and OCaml make every failure visible in the signature, while Python and Perl reach for thrown exceptions, and Go threads an explicit err value through each step.
“Perl itself is usually pretty good about telling you what you shouldn't do. :-)” — Larry Wall
# Two failure modes, both carried in the Result type — no exceptions.
sub safe_div($s: Str): Result[Int, Str] {
# parse_int returns Result[Int, Str]; ? propagates the Err early,
# map_err rewrites the parse error into our own message first
$n = parse_int($s).map_err(sub($e) { "not a number: $s" })?
if $n == 0 {
Err("cannot divide by zero")
} else {
Ok(100 / $n)
}
}
sub report($s: Str): Str {
match safe_div($s) {
Ok($v) { "100 / $s = $v" }
Err($e) { "error: $e" }
}
}
sub main(): Int {
print(report("4"))
print(report("0"))
print(report("oops"))
0
}guji has no exceptions: a fallible operation returns Result[T, E], and the postfix ? (§11.1) unwraps an Ok or returns the enclosing function's Err immediately. Because parse_int yields Result[Int, Str] already, map_err first adapts its error to our wording before ? propagates it. The caller is forced to match on Ok/Err, so every failure stays visible in the type signature — panic (§11.2) is reserved for genuine bugs, not for expected failure like this.
package main
import (
"errors"
"fmt"
"strconv"
)
func safeDiv(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("not a number: %s", s)
}
if n == 0 {
return 0, errors.New("cannot divide by zero")
}
return 100 / n, nil
}
func report(s string) string {
v, err := safeDiv(s)
if err != nil {
return fmt.Sprintf("error: %v", err)
}
return fmt.Sprintf("100 / %s = %d", s, v)
}
func main() {
fmt.Println(report("4"))
fmt.Println(report("0"))
fmt.Println(report("oops"))
}Go returns errors as an ordinary second value of type error, and the canonical if err != nil { return ... } check appears after every fallible call. There is no ? operator or exception, so each step's failure is threaded by hand — verbose but explicit. fmt.Errorf wraps a new message (and with %w could wrap the underlying error for later errors.Is/errors.As inspection), the standard way to add context as an error travels up.
let safe_div s =
match int_of_string_opt s with
| None -> Error (Printf.sprintf "not a number: %s" s)
| Some 0 -> Error "cannot divide by zero"
| Some n -> Ok (100 / n)
let report s =
match safe_div s with
| Ok v -> Printf.sprintf "100 / %s = %d" s v
| Error e -> Printf.sprintf "error: %s" e
let () =
List.iter (fun s -> print_endline (report s))
["4"; "0"; "oops"]OCaml's standard result type (Ok / Error) carries both failure modes, and int_of_string_opt returns an option rather than raising — the modern, total style over the exception-throwing int_of_string. Pattern matching destructures everything at once, and Some 0 lets the parse success and the zero check collapse into a single match. The let* binding operator from Result would give guji-style early return, but for two cases an explicit match reads clearest.
use strict;
use warnings;
sub safe_div {
my ($s) = @_;
die "not a number: $s\n" unless $s =~ /\A-?\d+\z/;
die "cannot divide by zero\n" if $s == 0;
return int(100 / $s);
}
sub report {
my ($s) = @_;
my $result = eval { safe_div($s) };
if ($@) {
chomp(my $err = $@);
return "error: $err";
}
return "100 / $s = $result";
}
print report("4"), "\n";
print report("0"), "\n";
print report("oops"), "\n";Perl's classic error mechanism is die/eval: safe_div throws a string with die, and the caller wraps the call in eval { ... }, inspecting the special $@ variable afterward to see whether it blew up. A trailing \n on the message suppresses Perl's automatic "at script line N" suffix. This is exceptions in disguise — newer code often uses Try::Tiny or throws blessed exception objects instead of bare strings for richer handling.
sub safe-div(Str $s) {
$s ~~ /^ '-'? \d+ $/ or fail "not a number: $s";
$s == 0 and fail "cannot divide by zero";
100 div $s;
}
sub report(Str $s) {
my $v = safe-div($s);
$v.defined ?? "100 / $s = $v" !! "error: { $v.exception.message }";
}
for <4 0 oops> -> $s {
say report($s);
}Raku's fail returns a lazy, unthrown Failure instead of dying immediately — touching it as a normal value (here testing .defined) keeps it tame, so report inspects .exception.message rather than wrapping a try. Raku also has genuine exceptions (die / CATCH / typed X:: classes) and an Either-like idiom, but fail is the lightweight "soft exception" tailored to exactly this recoverable case. The ~~ smartmatch validates the integer shape before the numeric == ever runs.
fn safe_div(s: &str) -> Result<i32, String> {
let n: i32 = s.parse().map_err(|_| format!("not a number: {s}"))?;
if n == 0 {
return Err("cannot divide by zero".to_string());
}
Ok(100 / n)
}
fn report(s: &str) -> String {
match safe_div(s) {
Ok(v) => format!("100 / {s} = {v}"),
Err(e) => format!("error: {e}"),
}
}
fn main() {
for s in ["4", "0", "oops"] {
println!("{}", report(s));
}
}Rust's Result<T, E> and the ? operator are the direct model guji follows: ? unwraps an Ok or returns the Err early, and map_err rewrites parse's error type to our String so ? type-checks. The exhaustive match in report forces both arms to be handled — the compiler rejects a forgotten case. Real code would use a typed error enum with thiserror instead of String, but the control flow is identical.
def safe_div(s):
try:
n = int(s)
except ValueError:
raise ValueError(f"not a number: {s}")
if n == 0:
raise ZeroDivisionError("cannot divide by zero")
return 100 // n
def report(s):
try:
return f"100 / {s} = {safe_div(s)}"
except (ValueError, ZeroDivisionError) as e:
return f"error: {e}"
print(report("4"))
print(report("0"))
print(report("oops"))Python leans entirely on exceptions: int(s) raises ValueError on bad input, and the zero case raises ZeroDivisionError explicitly. The caller wraps the call in try/except, catching a tuple of the two exception types and binding the instance as e. This "easier to ask forgiveness than permission" (EAFP) style is idiomatic Python — failures travel out-of-band rather than in the return type, the opposite of the Result-based languages here.