N

Nanyx

Documentation

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:

  1. Explicit over implicit: Errors are part of the type signature
  2. No surprises: Functions that can fail make it obvious in their return type
  3. Exhaustive handling: Pattern matching ensures you handle all cases
  4. 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.