Error Handling
Osprey has no language-level exceptions. Every fallible language operation
uses Result or a statically handled algebraic effect. A raw foreign status is
ABI data and MUST be translated at the safe Osprey boundary.
The two language flavors share these semantics. Examples show both surfaces where their syntax differs.
The Result Type
type Result<T, E> = Success { value: T } | Error { message: E }
The compiler rejects direct access to the contained value and never implicitly
converts Result<T, E> to T. Callers must pattern-match the Result (see
Pattern Matching) or use ?: to supply a fallback
(Result Default).
Assignments, arguments, comparisons, interpolation, and declared plain return
types do not erase the wrapper (Result Preservation).
let result = someFunctionThatCanFail()
let value = someFunctionThatCanFail() ?: 0
match result {
Success { value } => print("Success: ${value}")
Error { message } => print("Error: ${message}")
}
result = someFunctionThatCanFail
value = someFunctionThatCanFail ?: 0
match result
Success value => print "Success: ${value}"
Error message => print "Error: ${message}"
Arithmetic and Result — [ARITH-CHECKED]
Integer arithmetic is overflow checked. Integer +, -, *, unary -, and
abs return Result<int, MathError>; overflow returns
Error("integer overflow") and never wraps or panics. Floating-point +, -,
*, and unary - are plain IEEE-754 operations. Division and remainder return
Result<_, MathError> so they can report a zero divisor (and integer division's
unrepresentable minimum-value divided by -1 case).
| Operator | int, int | float, float | int, float / float, int |
|---|---|---|---|
+ - * |
Result<int, MathError> |
float |
float (int promoted before operation) |
/ |
Result<float, MathError> |
Result<float, MathError> |
Result<float, MathError> |
% |
Result<int, MathError> |
Result<float, MathError> |
Result<float, MathError> (int promoted) |
/ always yields float. Unary integer - has type
int -> Result<int, MathError>; unary float - has type float -> float.
The legacy builtins checkedAdd, checkedSub, and checkedMul remain safe
aliases returning Result<int, Error>.
Integer -9223372036854775808 % -1 returns Success(0); the representable
remainder is produced without executing LLVM's overflowing srem case.
let sum = 1 + 3 // Result<int, MathError>
let quotient = 10 / 3 // Result<float, MathError>
let remainder = 10 % 3 // Result<int, MathError>
let mixed = 10 + 5.5 // float
let checked = checkedAdd(a: 1, b: 3) // Result<int, Error>
let divZero = 10 / 0 // Error(division by zero)
sum = 1 + 3 // Result<int, MathError>
quotient = 10 / 3 // Result<float, MathError>
remainder = 10 % 3 // Result<int, MathError>
mixed = 10 + 5.5 // float
checked = checkedAdd (1, 3) // Result<int, Error>
divZero = 10 / 0 // Error(division by zero)
Chaining Arithmetic
An arithmetic chain such as (10 + 5) * 2 has one flattened
Result<int, MathError>, not a nested Result. Each operation runs only after
its Result operands succeed; the first Error is propagated unchanged and
later operations are not evaluated. This failure-preserving flattening is the
only context that may consume a Result payload without an explicit match or
?:, and it may only flatten the common MathError channel. It is not a
conversion from Result<T, E> to T (Result Preservation).
match (10 + 5) / 2 {
Success { value } => print("Final: ${value}")
Error { message } => print("error: ${message}")
}
match (10 + 5) / 2
Success value => print "Final: ${value}"
Error message => print "error: ${message}"
Choosing and preserving a policy
?: is the concise form only when replacing every error with one fallback is
the intended policy. Use an exhaustive match when the diagnostic must remain
available. An expected string never causes a successful integer payload to
be coerced to text and never turns the error branch into a string; both branches
must be mapped explicitly into a new Result.
fn renderSum(a, b) = match a + b {
Success { value } => Success { value: toString(value) }
Error { message } => Error { message: message }
}
renderSum (a, b) = match a + b
Success value => Success(value = toString value)
Error message => Error(message = message)
An algebraic effect may centralize a larger region's policy. The effect must
carry the original diagnostic and return a Result; its handler is statically
required and may preserve or deliberately recover from the error. Function
return types and effect rows in this example are inferred.
effect ArithmeticFailure {
decide: fn(string) -> Result<int, MathError>
}
fn addThroughPolicy(a, b) = match a + b {
Success { value } => Success { value: value }
Error { message } => perform ArithmeticFailure.decide(message)
}
fn preserveFailure(a, b) = handle ArithmeticFailure
decide message => Error { message: message }
in addThroughPolicy(a, b)
effect ArithmeticFailure
decide : string => Result<int, MathError>
addThroughPolicy (a, b) = match a + b
Success value => Success(value = value)
Error message => perform ArithmeticFailure.decide message
preserveFailure (a, b) =
handle ArithmeticFailure
decide message => Error(message = message)
in addThroughPolicy (a, b)
Executable preserve-and-recover cases for both flavors live in
tests/core/arithmetic/effect_policies.test.osp{,ml}.
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)"
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, not a placeholder, static string, or default.
match split("abc", "") {
Success { value } => forEachList(value, print)
Error { message } => print(message) // "split: separator must not be empty"
}
match split ("abc", "")
Success value => forEachList value print
Error message => print message // "split: separator must not be empty"
This requirement applies to every Result-returning operator, builtin, and
user function, including failure-preserving arithmetic chains.