Nanyx

Error handling

Working with Result and Option types

In Nanyx, errors are values: a function that can fail says so in its return type, using tag unions, so callers must handle the failure cases. There is no catchable exception mechanism. A program can still panic on an unrecoverable failure (such as running out of memory), but a panic is not something code is expected to catch or recover from.

Result types

In Nanyx, functions that can fail return a result type using tag unions:

-- A function that might fail
def getCustomer: CustomerId -> #some(Customer) | #error(#notFound | #databaseError)
  = { id -> ... }

Handle the result with pattern matching:

match getCustomer(someId)
  | #some(customer) -> println("Found: {customer.name}")
  | #error(#notFound) -> println("Customer not found")
  | #error(#databaseError) -> println("Database error")

The try keyword

proc is the standard library’s result workflow. Inside a proc block, try short-circuits on errors and return wraps the final value in #some:

def result = proc {
  def customer = try getCustomer("someid")
  def latestOrder = try getLatestOrder(customer)
  return latestOrder.total
}
-- `result` has type `#some(int) | #error(#notFound | #databaseError | #noOrders)`

If any step returns an error, the function immediately returns that error.

Option types

For values that might not exist, use the #some / #none pattern:

def findUser: int -> #some(User) | #none = { id ->
  match id
    | 1 -> #some(User(name = "Alice"))
    | _ -> #none
}

The except keyword

The except keyword is a match that doesn’t require exhaustive handling: any case you don’t handle passes through unchanged. The result type is therefore the input union minus the handled tags. Each handled tag has an arm, and the arm’s value must fit that narrowed type.

Because the narrowed type often has a single case left, except combines naturally with a destructuring binding, which requires its pattern to be total:

def g = {
  def f: () -> #some(int) | #error(#notFound) = ...

  -- After `except`, the only remaining case is `#some(int)`, so the
  -- pattern `#some(result)` is total and `result` has type `int`
  def #some(result) = f() except #error(#notFound) -> #some(0)

  result
}

Inside a proc block the arm can exit early instead of supplying a replacement value:

def g = proc {
  def #some(result) = f() except #error(#notFound) -> return 0
  return result
}

Chaining with pipelines

input
  \parseInt
  \Result.map { * 2 }
  \Result.defaultValue(0)

Philosophy: By making errors explicit in the type system, Nanyx ensures you always handle failure cases. No more unexpected crashes!