Error Handling
Nostos promotes robust error handling through its type system, favoring explicit error representation over exceptions for recoverable errors. For unrecoverable errors, a panic mechanism is available, and for boundary situations, try/catch blocks can be used.
The Option Type (No Nulls!)
Nostos does not have null or nil. Instead, it uses the Option[T] type to represent the possible absence of a value. This forces you to explicitly handle both cases, eliminating a common source of bugs.
# Option[T] is built-in: Some(T) | None()
find_item(list, target) =
if contains(list, target) then Some(target)
else None()
main() = {
my_list = [1, 2, 3]
found = find_item(my_list, 2)
not_found = find_item(my_list, 4)
# Handle Option using pattern matching
result1 = match found {
Some(val) -> "Found: " ++ show(val)
None() -> "Not found"
}
println(result1) # "Found: 2"
result2 = match not_found {
Some(val) -> "Found: " ++ show(val)
None() -> "Not found"
}
println(result2) # "Not found"
}
Option Helper Methods
The standard library provides convenient methods for working with Option values. These can be called using method syntax on Option values.
use stdlib.list.*
main() = {
some_val = Some(10)
none_val: Option[Int] = None()
# map - transform the value inside Some
doubled = some_val.map(x => x * 2) # Some(20)
# flatMap - chain operations that return Option
result = some_val.flatMap(x => if x > 5 { Some(x + 100) } else { None() }) # Some(110)
# unwrap - get value or panic if None
val = some_val.unwrap() # 10
# unwrapOr - get value or return default
val1 = some_val.unwrapOr(0) # 10
val2 = none_val.unwrapOr(0) # 0
# isSome / isNone - check if value present
if some_val.isSome() { println("Has value") }
if none_val.isNone() { println("No value") }
}
Option Methods Summary
opt.map(fn)- Transform value: Some(x) → Some(fn(x)), None → Noneopt.flatMap(fn)- Chain: Some(x) → fn(x), None → None (where fn returns Option)opt.unwrap()- Get value or panicopt.unwrapOr(default)- Get value or return defaultopt.isSome()- Returns true if Someopt.isNone()- Returns true if None
The Result Type (Recoverable Errors)
For operations that can fail in a way you might want to recover from, Nostos uses the Result[T, E] type. It's either Ok(T) for a successful result or Err(E) for an error.
# Result[T, E] is built-in: Ok(T) | Err(E)
# Custom error type as a variant
type DivisionError = DivideByZero(String)
safe_divide(numerator, denominator) =
if denominator == 0 then Err(DivideByZero("Cannot divide by zero"))
else Ok(numerator / denominator)
main() = {
division_ok = safe_divide(10, 2)
division_err = safe_divide(10, 0)
result_ok = match division_ok {
Ok(val) -> "Result: " ++ show(val)
Err(DivideByZero(msg)) -> "Error: " ++ msg
}
println(result_ok) # "Result: 5"
result_err = match division_err {
Ok(val) -> "Result: " ++ show(val)
Err(DivideByZero(msg)) -> "Error: " ++ msg
}
println(result_err) # "Error: Cannot divide by zero"
}
Result Helper Methods
The standard library provides convenient methods for working with Result values, similar to Option. These can be called using method syntax on Result values.
use stdlib.list.*
main() = {
ok_val: Result[Int, String] = Ok(42)
err_val: Result[Int, String] = Err("something went wrong")
# map - transform the success value
doubled = ok_val.map(x => x * 2) # Ok(84)
err_doubled = err_val.map(x => x * 2) # Err("something went wrong")
# mapErr - transform the error value
new_err = err_val.mapErr(e => "Error: " ++ e) # Err("Error: something went wrong")
# flatMap - chain operations that return Result
chained = ok_val.flatMap(x => if x > 0 { Ok(x * 10) } else { Err("negative") }) # Ok(420)
# unwrap - get value or panic if Err
val = ok_val.unwrap() # 42
# unwrapOr - get value or return default
val1 = ok_val.unwrapOr(0) # 42
val2 = err_val.unwrapOr(0) # 0
# isOk / isErr - check result status
if ok_val.isOk() { println("Success!") }
if err_val.isErr() { println("Failed!") }
# toOption - convert Result to Option (discards error)
opt = ok_val.toOption() # Some(42)
opt2 = err_val.toOption() # None()
}
Result Methods Summary
res.map(fn)- Transform success: Ok(x) → Ok(fn(x)), Err(e) → Err(e)res.mapErr(fn)- Transform error: Ok(x) → Ok(x), Err(e) → Err(fn(e))res.flatMap(fn)- Chain: Ok(x) → fn(x), Err(e) → Err(e) (where fn returns Result)res.unwrap()- Get value or panicres.unwrapOr(default)- Get value or return defaultres.isOk()- Returns true if Okres.isErr()- Returns true if Errres.toOption()- Converts Ok(x) to Some(x), Err to None
The ? Operator (Error Propagation)
The ? operator provides a convenient shorthand for propagating errors from functions that return Result. If the Result is Err, it returns the error immediately; otherwise, it unwraps the Ok value.
# (Assume safe_divide and DivideByZeroError from above)
calculate_complex(a, b, c) = {
val1 = safe_divide(a, b)? # Returns Err if b is 0
val2 = safe_divide(val1, c)? # Returns Err if c is 0
Ok(val1 + val2)
}
main() = {
res1 = calculate_complex(10, 2, 1) # Ok(7)
res2 = calculate_complex(10, 0, 1) # Err(DivideByZeroError) from first call
res3 = calculate_complex(10, 2, 0) # Err(DivideByZeroError) from second call
println(res1)
println(res2)
println(res3)
}
Throw (Catchable Exceptions)
Use throw to raise exceptions that can be caught with try/catch. You can throw any value - strings, numbers, or custom error types.
# Throw with a string message
safe_divide(a, b) =
if b == 0 then throw("division by zero")
else a / b
# Throw with a custom error type
type ApiError = { code: Int, msg: String }
fetch_user(id) =
if id < 0 then throw(ApiError(404, "not found"))
else "User " ++ id.show()
main() = {
# These throws can be caught with try/catch
result = try { safe_divide(10, 0) } catch { e -> "Error: " ++ e }
println(result) # "Error: division by zero"
}
Panic (Unrecoverable Errors)
Use panic for truly unrecoverable situations - programmer errors, violated invariants, or bugs that should never happen. Unlike throw, panics indicate logic errors that shouldn't be caught and handled normally.
# Panic on invariant violation
validate_positive(n) =
if n <= 0 then panic("Expected a positive number, got " ++ n.show())
else n
# Pattern matching exhaustiveness
unwrap(Some(x)) = x
unwrap(None) = panic("called unwrap on None")
main() = {
valid = validate_positive(5)
# This will cause a panic and terminate the process
# invalid = validate_positive(-1)
}
throw vs panic
throw- Expected errors that callers should handle (invalid input, network failure, file not found)panic- Bugs and invariant violations that should never occur in correct code
try/catch (Boundary Error Handling)
While Result is for recoverable errors in pure code, try/catch provides a mechanism to catch and handle exceptions. It's primarily used for I/O operations and when interacting with external systems.
risky_operation(should_panic) =
if should_panic then panic("Something went really wrong!")
else "Operation successful"
main() = {
result1 = try { risky_operation(false) }
catch { err -> "Caught error: " ++ err.show() }
println(result1) # "Operation successful"
result2 = try { risky_operation(true) }
catch { err -> "Caught error: " ++ err.show() }
println(result2) # "Caught error: Something went really wrong!"
}
I/O Error Handling
All I/O operations in Nostos (file, network, HTTP) throw exceptions on error. This keeps the happy path clean while ensuring errors are handled explicitly with try/catch.
# File I/O with exception handling
read_config(path) = {
try {
content = File.readAll(path)
println("Config loaded: " ++ content)
Some(content)
} catch { e -> {
println("Failed to read config: " ++ e)
None
} }
}
# HTTP requests with error handling
fetch_data(url) = {
try {
response = Http.get(url)
if response.status == 200 then
Some(response.body)
else
None
} catch { e -> {
println("Network error: " ++ e)
None
} }
}
# Combining multiple I/O operations
process_file(input_path, output_path) = {
try {
data = File.readAll(input_path)
processed = String.toUpper(data)
File.writeAll(output_path, processed)
println("Processing complete!")
true
} catch { e -> {
println("Error: " ++ e)
false
} }
}
main() = {
read_config("/etc/myapp/config.txt")
fetch_data("https://api.example.com/data")
process_file("/tmp/input.txt", "/tmp/output.txt")
}
Design Note: I/O operations throw exceptions because they interact with the external world where failures are common (network timeouts, missing files, permission errors). Using exceptions keeps your code clean while ensuring you explicitly handle failure cases at appropriate boundaries.