Error Handling
Nanyx takes a different approach to error handling than many languages. There's no null, no exceptions, and no try-catch. Instead, Nanyx uses explicit results with tag unions to model failures.
No Null Values
Nanyx has no concept of null. If you want to represent a value that may be missing, use a tag union:
type Option(a) = #some(a) | #none
def findUser: UserId -> Option(User) = { id ->
-- lookup logic
#some(user) -- or #none if not found
}Result Types
The most common pattern for error handling is the Result type:
type Result(a, e) = #ok(a) | #error(e)
def divide: (int, int) -> Result(int, string) = { x, y ->
if y == 0
-> #error("Division by zero")
else -> #ok(x / y)
}Pattern Matching on Results
Handle results explicitly with pattern matching:
def processResult = { result ->
match result
| #ok(value) -> process(value)
| #error(msg) -> logError(msg)
}Chaining Operations
You can chain operations on Results:
def processData: string -> Result(ProcessedData, string) = { input ->
input
\parse
\map(validate)
\map(transform)
}Custom Error Types
Use tag unions to create rich error types:
type ValidationError =
| #invalidEmail(string)
| #tooShort(int)
| #tooLong(int)
| #missingField(string)
def validateUser: User -> Result(User, ValidationError) = { user ->
if not (user.email \contains("@"))
-> #error(#invalidEmail(user.email))
if user.name.length < 3
-> #error(#tooShort(user.name.length))
else
-> #ok(user)
}Error Handling with Contexts
For more complex scenarios, use contexts to handle errors:
context Raise(e) = (
raise: e -> a
)
def safeDivide: <Raise(string)> (int, int) -> int = { x, y ->
if y == 0
-> raise("Division by zero")
else -> x / y
}
def handleErrors = {
use Raise(
raise = { msg ->
println("Error: {msg}")
resume 0 -- provide default value
}
)
safeDivide(10, 0) -- Returns 0 instead of crashing
}Multiple Error Paths
Use tag unions to represent different failure modes:
type FileError =
| #notFound
| #permissionDenied
| #corruptedData
type NetworkError =
| #timeout
| #connectionLost
| #serverError(int)
type AppError =
| #file(FileError)
| #network(NetworkError)
| #validation(ValidationError)The Option Type in Detail
The Option type is widely used for values that might not exist:
type Option(a) = #some(a) | #none
-- Finding in a list
def findFirst: (list(a), (a -> bool)) -> Option(a) = { xs, predicate ->
match xs
| [] -> #none
| [head, ...tail] ->
if predicate(head)
-> #some(head)
else -> findFirst(tail, predicate)
}
-- Using the result
def result = findFirst([1, 2, 3], { x -> x > 2 })
match result
| #some(x) -> println("Found: {x}")
| #none -> println("Not found")Mapping Over Results
Transform the success value without unwrapping:
def doubled = result \map { * 2 }
-- If result is #ok(5), doubled is #ok(10)
-- If result is #error(msg), doubled is #error(msg)Philosophy
Nanyx's approach to error handling:
- Explicit over implicit: Errors are part of the type signature
- No surprises: Functions that can fail make it obvious in their return type
- Exhaustive handling: Pattern matching ensures you handle all cases
- Composable: Results can be chained and transformed
Unlike exceptions which can be thrown anywhere and caught anywhere, Nanyx's error handling is local and explicit. You always know which functions might fail by looking at their types.