Rayforce ← Back to home
GitHub

Error Handling

Catching errors with try/raise, understanding error types, null propagation, and writing defensive Rayfall code.

Rayfall provides structured error handling through try and raise, three distinct null representations for missing data, and consistent error propagation rules. This guide covers every pattern you need for robust Rayfall programs.

1. try/raise Basics

The try special form evaluates an expression and, if it produces an error, calls a handler function with the error value. The raise function creates an error explicitly.

Basic Syntax

(try expr handler)

Both arguments are passed unevaluated (try is a special form). The handler is only evaluated and called if expr produces an error.

Catching Division by Zero

(try
  (/ 10 0)
  (fn [err] (println "caught:" err)))
; caught: 0

When an error occurs without an explicit raise, the handler receives a default value of 0.

Raising Custom Errors

(try
  (raise "invalid input")
  (fn [err] (println "error:" err)))
; error: "invalid input"

With raise, the handler receives the exact value you raised — a string, number, or any Rayfall object.

Returning a Default Value

(try
  (/ 10 0)
  (fn [e] -1))
; => -1

The result of try is either the successful result of expr or the return value of the handler. This makes try an expression you can embed anywhere.

Raising Structured Errors

;; raise any value, not just strings
(try
  (raise 42)
  (fn [e] (+ e 8)))
; => 50

You can raise any Rayfall value. The handler receives it directly, so you can raise numbers, vectors, or even tables if you want structured error information.

When No Error Occurs

(try
  (+ 2 3)
  (fn [e] 0))
; => 5

If the expression succeeds, the handler is never called and the result passes through unchanged.

2. Error Types

Rayfall produces several categories of runtime errors. Understanding these helps you write better handlers.

Type Errors

Occur when an operation receives the wrong argument type:

(+ 1 "hello")
; error: type

Arithmetic, comparison, and most builtins check argument types and produce type errors for mismatches.

Arity Errors

Occur when a function receives the wrong number of arguments:

(+ 1)
; error: arity — + expects 2 arguments

Domain Errors

Occur when arguments have valid types but invalid values:

(/ 10 0)
; error: domain — division by zero

(take 100 [1 2 3])
; takes as many as available (no error for over-take)

User-Raised Errors

Any error you create with raise:

(raise "custom error message")
; error: domain

Internally, raise stores your value and triggers a domain error. The try handler receives the stored value.

Access Errors (Restricted Mode)

In restricted evaluation mode (used for IPC), certain functions are blocked:

; on a restricted server connection:
(.sys.exec "rm -rf /")
; error: access — restricted

How Errors Display in the REPL

Unhandled errors print with a category prefix:

rf> (/ 1 0)
error: domain

rf> (+ 1 "x")
error: type

Errors are terminal — an unhandled error stops evaluation of the current expression and returns to the REPL prompt.

3. Null Propagation

Missing data is a first-class concept in Rayforce. There are three distinct null forms, each with different behavior.

The Three Null Forms

Quick reference:
  • RAY_NULL_OBJ — the void return value (from println, show)
  • Typed nulls0Ni (int), 0Nf (float), 0Nd (date) — missing values in data
  • Null bitmaps — per-element null flags on vectors

Typed Null Literals

Typed nulls represent missing data within a specific type. They look like values but carry a null flag:

;; Typed null literals
0Ni   ; null integer
0Nf   ; null float
0Nd   ; null date

Null Propagation Through Arithmetic

Typed nulls propagate through arithmetic — any operation involving a typed null produces a typed null:

(+ 1 0Ni)
; => 0Nl (promoted to i64 null because 1 is i64)

(* 3.14 0Nf)
; => 0Nf

(+ 0Ni 0Ni)
; => 0Ni

This is the standard SQL/database behavior: null in, null out. It prevents silent corruption of calculations with missing data.

RAY_NULL_OBJ (Void Return)

Some operations like println and show return a void null. This is different from typed nulls — using it in arithmetic produces a type error, not propagation:

(set x (println "hello"))
; hello
(+ x 1)
; error: type

Nulls in Vectors

Vectors track null elements via a compact bitmap. Null elements propagate through vectorized operations:

(set v [1 0Ni 3 4 0Ni])
(+ v 10)
; => [11 0Ni 13 14 0Ni]

All Nulls Are Falsy

Every null form — void null, typed nulls, and null vector elements — evaluates to false in conditional contexts:

(if 0Ni "truthy" "falsy")
; => "falsy"

(if 0Nf "truthy" "falsy")
; => "falsy"

Testing for Null with nil?

The nil? function detects all null forms:

(nil? 0Ni)
; => 1

(nil? 42)
; => 0

(nil? (println "x"))
; x
; => 1

nil? is one of the few functions that safely handles the void null without producing a type error.

Null Propagation in Arithmetic

Arithmetic with typed nulls propagates the null through:

(+ [1 0Ni 3] [10 20 30])
; => [11 0Ni 33]  — null element stays null

(sum [1 0Ni 3])
; => 4  — aggregates skip nulls

4. Defensive Patterns

Practical patterns for writing Rayfall code that handles edge cases gracefully.

Check for Null Before Operating

(set val 0Ni)  ; could be a result from a lookup
(if (nil? val)
  0
  (* val 2))
; returns 0 for null, otherwise doubles the value

Default Value Pattern

;; define a "default" helper
(set with-default (fn [x d]
  (if (nil? x) d x)))

(with-default 0Ni -1)
; => -1

(with-default 42 -1)
; => 42

Safe Division

(set safe-div (fn [a b]
  (if (== b 0)
    0Nf
    (/ a b))))

(safe-div 10 3)
; => 3.333...

(safe-div 10 0)
; => 0Nf

Guard with try for File I/O

;; safely load a CSV, return empty table on failure
(set data
  (try
    (.csv.read "data.csv")
    (fn [e]
      (println "failed to load:" e)
      (table [x] (list [])))))
; if file missing: prints error, returns empty table

Validate Table Columns Before Querying

;; check that required columns exist
(set validate-cols (fn [tbl required]
  (set have (key tbl))
  (map (fn [c]
    (if (nil? (find have c))
      (raise (format "missing column: {}" c))))
    required)))

;; use it before a query
(try
  (do
    (validate-cols trades ['sym 'price 'qty])
    (select {from: trades by: {sym: sym} cols: {total: (sum qty)}}))
  (fn [e] (println "validation failed:" e)))

5. Error Recovery in Pipelines

When building data pipelines, errors in one stage should not always halt the entire computation.

Errors in select/update

If a computed column expression fails inside select, the entire select produces an error. Wrap the select in try to recover:

(set trades (table [sym price qty]
  (list [AAPL GOOG MSFT]
        [150.5 2800.0 310.0]
        [100 50 75])))

;; safe query wrapper
(set safe-query (fn [q]
  (try q
    (fn [e]
      (println "query failed:" e)
      0Ni))))

Processing Rows with Error Tolerance

;; process each row, collecting errors separately
(set results
  (map (fn [row]
    (try
      (/ (get row revenue) (get row shares))
      (fn [e] 0Nf)))
    rows))
; failed rows get 0Nf, successful rows get the result

Chaining try for Multi-Step Pipelines

;; load -> transform -> aggregate, with recovery at each step
(set result
  (try
    (do
      (set raw (.csv.read "input.csv"))
      (set clean (select {from: raw where: (> price 0)}))
      (select {from: clean by: {sym: sym} cols: {p: (avg price)}}))
    (fn [e]
      (println "pipeline failed:" e)
      0Ni)))

Nested try for Granular Recovery

;; inner try catches load failure, outer catches query failure
(try
  (do
    (set data
      (try
        (.csv.read "primary.csv")
        (fn [e]
          (println "primary failed, trying backup")
          (.csv.read "backup.csv"))))
    (select {from: data by: {sym: sym} cols: {total: (sum qty)}}))
  (fn [e] (println "all attempts failed:" e)))

6. Quick Reference

Pattern Syntax Behavior
Catch errors (try expr handler) Calls handler with error value on failure
Raise error (raise value) Triggers error, value passed to nearest try handler
Test for null (nil? x) Returns 1 for any null form, 0 otherwise
Typed null int 0Ni Propagates through arithmetic
Typed null float 0Nf Propagates through arithmetic
Typed null date 0Nd Propagates through date operations
Null in if (if 0Ni ...) All nulls are falsy
Default value (if (nil? x) d x) Substitute default for missing data

Next Steps