Result@(T, E)¶
Type-safe error handling with explicit success and error types.
Overview¶
Result@(T, E) is a generic enum that represents either success (Ok) containing a value of type T, or failure (Err) containing an error of type E.
All functions in Sushi implicitly return Result@(T, E) where:
- T is the declared return type
- E is the error type (defaults to StdError if not specified)
Type Syntax¶
Implicit Return with Default Error¶
fn add(i32 a, i32 b) i32:
return Result.Ok(a + b)
# Actually returns Result@(i32, StdError)
Custom Error Type¶
enum MathError:
DivisionByZero
Overflow
fn divide(i32 a, i32 b) i32 | MathError:
if (b == 0):
return Result.Err(MathError.DivisionByZero)
return Result.Ok(a / b)
# Returns Result@(i32, MathError)
Explicit Syntax¶
fn foo() Result@(i32, MyError):
return Result.Ok(42)
Standard Error Enums¶
Sushi provides built-in error types. Each but StdError has a HOME module, and the import
is what brings the bare name into a unit (docs/design/unit-namespaces.md, #574). A
module whose calls answer an enum re-exports its home (#586), so the import a program
already writes is the one that brings the name:
| enum | home | also brought by |
|---|---|---|
StdError |
none -- the implicit Result arm is in scope everywhere | |
MathError |
use <math> |
|
FileMode |
use <io/fs> |
|
IoError, FileError |
use <io/error> |
<io/contracts>, <io/fs>, <io/buf>, <io/files>, <net/tcp> |
SeekFrom |
use <io/contracts> |
<io/fs>, <io/buf>, <net/tcp> |
NetError |
use <net/error> |
<net/tcp>, <net/udp>, <net/dns>, <net/ip> |
ProcessError |
use <sys/process> |
|
EnvError |
use <sys/env> |
StdError¶
Generic fallback for simple errors.
StdError.Error- Generic error condition
MathError¶
Mathematical operation errors.
MathError.DivisionByZero- Division by zeroMathError.Overflow- Arithmetic overflowMathError.Underflow- Arithmetic underflowMathError.InvalidInput- Invalid mathematical input
FileError¶
File system operation errors.
FileError.NotFound- File does not existFileError.PermissionDenied- Insufficient permissionsFileError.AlreadyExists- File already existsFileError.IsDirectory- Path refers to a directoryFileError.DiskFull- No space left on deviceFileError.TooManyOpen- Too many open filesFileError.InvalidPath- Invalid file pathFileError.IOError- Generic I/O errorFileError.Other- Any other error
IoError¶
I/O operation errors.
IoError.ReadError- Read operation failedIoError.WriteError- Write operation failedIoError.FlushError- Flush operation failed
ProcessError¶
Process management errors.
ProcessError.SpawnFailed- Failed to spawn processProcessError.ExitFailure- Process exited with errorProcessError.SignalReceived- Process terminated by signal
EnvError¶
Environment variable errors.
EnvError.NotFound- Environment variable not foundEnvError.InvalidValue- Invalid environment variable valueEnvError.PermissionDenied- Insufficient permissions
Constructors¶
Result.Ok(value)¶
Create a success result containing a value.
fn get_answer() i32:
return Result.Ok(42)
Result.Err(error)¶
Create an error result containing an error value.
fn divide(i32 a, i32 b) i32 | MathError:
if (b == 0):
return Result.Err(MathError.DivisionByZero)
return Result.Ok(a / b)
Important: Result.Err() requires an error value. Calling it with zero arguments is a compile error (CE2050 — wrong argument count for the Err variant), not a deprecation warning.
Methods¶
.is_ok() -> bool¶
Check if the Result is an Ok variant.
let Result@(i32, MathError) result = divide(10, 2)
if (result.is_ok()):
println("Success!")
.is_err() -> bool¶
Check if the Result is an Err variant.
let Result@(i32, MathError) result = divide(10, 0)
if (result.is_err()):
println("Division failed")
.err() -> Maybe@(E)¶
Extract the error value if present, otherwise return Maybe.None().
let Result@(i32, MathError) result = divide(10, 0)
let Maybe@(MathError) error = result.err()
match error:
Maybe.Some(e) ->
println("Error occurred: {e}")
Maybe.None() ->
println("No error")
.expect(message: string) -> T¶
Unwrap the Ok value or panic with the given message if Err.
let Result@(i32, MathError) result = divide(10, 2)
let i32 value = result.expect("Division should not fail")
# Prints "ERROR: Division should not fail" and exits if Err
Warning: Use expect() sparingly. It will terminate the program if the Result is Err.
.realise(default: T) -> T¶
Extract the Ok value or return a default value if Err.
let Result@(i32, MathError) result = divide(10, 0)
let i32 value = result.realise(0) # Returns 0 on error
Error Propagation with ??¶
The ?? operator unwraps a Result or propagates the error to the caller.
fn compute() i32 | MathError:
let i32 x = divide(10, 2)?? # Unwraps or returns early
let i32 y = divide(20, 5)??
return Result.Ok(x + y)
Error Type Matching¶
The ?? operator requires error types to match exactly:
enum ErrorA:
Error
enum ErrorB:
Error
fn inner() i32 | ErrorA:
return Result.Ok(42)
fn outer() i32 | ErrorB:
let i32 x = inner()?? # ❌ Error: cannot propagate ErrorA to ErrorB
return Result.Ok(x)
To use ??, the inner function's error type must match the outer function's error type:
fn outer() i32 | ErrorA:
let i32 x = inner()?? # ✅ Both use ErrorA
return Result.Ok(x)
Warning: Do NOT Use ?? in main()¶
Using ?? in the main() function generates a compiler warning and is highly discouraged:
fn main() i32:
let i32 x = risky()?? # ⚠️ Warning CW2511
return Result.Ok(0)
Instead, use explicit error handling:
fn main() i32:
match risky():
Result.Ok(x) ->
println("Got: {x}")
return Result.Ok(0)
Result.Err(e) ->
println("Failed")
return Result.Ok(1)
Pattern Matching¶
Match on both success and error cases:
match divide(10, 2):
Result.Ok(value) ->
println("Result: {value}")
Result.Err(MathError.DivisionByZero) ->
println("Cannot divide by zero")
Result.Err(e) ->
println("Other error: {e}")
Usage in Conditionals¶
Result can be used directly in if statements (checks for Ok):
if (divide(10, 2)):
println("Success!")
else:
println("Failed")
Best Practices¶
- Always handle errors explicitly - Don't ignore Result values
- Use
??for error propagation - In function chains with matching error types - Use
.realise(default)for fallback values - When a default makes sense - Use pattern matching for detailed error handling - When you need different behavior per error variant
- Avoid
expect()in production code - It terminates the program on error - Avoid
??in main() - Use explicit error handling instead - Keep error types consistent - Makes error propagation easier
- Define custom error enums - For domain-specific error conditions
Examples¶
Basic Error Handling¶
enum ValidationError:
TooShort
TooLong
InvalidCharacters
fn validate_username(string name) ~ | ValidationError:
if (name.len() < 3):
return Result.Err(ValidationError.TooShort)
if (name.len() > 20):
return Result.Err(ValidationError.TooLong)
return Result.Ok(~)
Error Propagation Chain¶
fn read_config() string | FileError:
let File f = open("config.txt", FileMode.Read())??
let string content = f.read_all()??
return Result.Ok(content)
Combining with Maybe¶
fn safe_divide(i32 a, i32 b) i32 | MathError:
if (b == 0):
return Result.Err(MathError.DivisionByZero)
return Result.Ok(a / b)
fn process() i32 | MathError:
let Result@(i32, MathError) result = safe_divide(10, 2)
let Maybe@(MathError) error = result.err()
if (error.is_some()):
return Result.Err(error.realise(MathError.DivisionByZero))
let i32 value = result.realise(0)
return Result.Ok(value)