Error Handling
Osprey has no exceptions, panics, or null. Any function that can fail returns a Result.
Status
[ERR-PAYLOAD] conforms for E = string: the runtime Result block carries a
dedicated i8* errmsg slot, Error { message } binds the real reason, and
toString renders Error(<reason>). Discriminated-union error payloads
(Result<T, StringError>) remain deferred behind
recursive-union-payloads.md.
The Result Type
type ResultT, E> = Success { value: T } | Error { message: E }
The compiler rejects any direct access to the contained value. Callers must pattern-match the Result (see Pattern Matching) unless one of the auto-unwrap contexts applies (Result Auto-Unwrapping):
let result = someFunctionThatCanFail()
match result {
Success { value } => print("Success: ${value}")
Error { message } => print("Error: ${message}")
}
Arithmetic Returns Result
Every arithmetic operation returns Result<T, MathError> so overflow, underflow, and division by zero surface as values, not panics.
| Operator | int, int | float, float | int, float / float, int |
|---|---|---|---|
+ - * % |
Result<int, MathError> |
Result<float, MathError> |
Result<float, MathError> (int promoted) |
/ |
Result<float, MathError> |
Result<float, MathError> |
Result<float, MathError> |
/ always yields float. There is no implicit int/float conversion outside this table; use toFloat and toInt for explicit conversion.
let sum = 1 + 3 // Result
let quotient = 10 / 3 // Result
let remainder = 10 % 3 // Result
let mixed = 10 + 5.5 // Result
let divZero = 10 / 0 // Error(DivisionByZero)
Chaining Arithmetic
Compound expressions auto-unwrap intermediate Results — (10 + 5) * 2 is a single Result<int, MathError>, never a nested one, and only the final value is matched (Result Auto-Unwrapping):
match (10 + 5) * 2 {
Success { value } => print("Final: ${value}")
Error { message } => print("error: ${message}")
}
toString Format
A Result formats as Success(<value>) or Error(<message>):
print(toString(15 / 3)) // "Success(5.0)" — division is always float
print(toString(10 / 0)) // "Error(division by zero)"
Error Payload Propagation — [ERR-PAYLOAD]
When a function produces Error { message: E }, the value bound to message in the caller's match arm MUST be the exact E value that the producer wrote — never a placeholder, never a static string, never a default. The discriminant ("this Result is an Error") and the payload ("what went wrong") are both part of the value; throwing away one defeats the type.
match split("abc", "") {
Success { value } => forEach(value, print)
Error { message } => print(message) // MUST print "separator is empty",
// not "Error occurred"
}
This requirement applies uniformly across arithmetic, string, list, map, file-I/O, HTTP, and user-defined fallible functions, and to nested Result chains (auto-unwrap MUST preserve the original error payload). Implementations that lose the payload — for example by binding the pattern variable to a static global — are non-conforming.