Error Handling in Topaz
Topaz v1.0 is designed with a strong emphasis on creating robust and predictable software. To achieve this, it favors explicit error handling using Algebraic Data Types (ADTs)--primarily the Result
and Option
types--over traditional, unchecked exceptions for most recoverable errors.
This approach makes potential failures a visible and integral part of a process's signature, forcing developers to acknowledge and handle them thoughtfully, which leads to more resilient applications.
The Result
Type
A Result
represents one of two possible outcomes of an operation: success or failure. This is the primary tool for handling operations that can fail.
(result:ok value)
: Contains the successful result value. For example,(result:ok 42)
.(result:err error-info)
: Contains an error value (e.g., a string or a more structured error type) explaining what went wrong. For example,(result:err "File not found")
.
A function that might fail is designed to return a Result
type, making its behavior clear to anyone who uses it.
Example: A Safe Division Process
;; A process for "safe division" that returns a Result type.
($ safe-divide
(λ (numerator denominator)
(IF (= denominator 0)
(result:err "Division by zero is not allowed.") ;; Return an error on failure
(result:ok (/ numerator denominator)) ;; Return success with value
)
)
)
($ result1 (safe-divide 10 2)) ;; Evaluates to (result:ok 5)
($ result2 (safe-divide 10 0)) ;; Evaluates to (result:err "Division by zero is not allowed.")
Working with Result
Values
You must "unwrap" or handle a Result
to get to the inner value. This is typically done with conditional logic or, in future versions, more advanced pattern matching.
Example: Handling a Result
($ handle-division-result
(λ (result)
(IF (result:is-ok result) ;; Check if result is success
(io:print (string:concat "Success! The value is: " (string:to-string (result:get-value result))))
(io:print (string:concat "An error occurred: " (result:get-error result)))
)
)
)
(handle-division-result result1) ;; Prints "Success! The value is: 5"
(handle-division-result result2) ;; Prints "An error occurred: Division by zero is not allowed."
Note: result:ok
, result:err
, result:is-ok
, result:get-value
, and result:get-error
are the constructors and helper functions for the Result
type that are part of the standard library.
Chaining Fallible Operations
The real power of the Result
type shines when you need to perform a sequence of operations where any step can fail. The model allows you to chain these operations elegantly, short-circuiting the entire sequence as soon as an error occurs.
Conceptual Example:
;; A hypothetical 'result:then' process takes a Result and a process.
;; It only applies the process if the Result is OK.
;; If the Result is ERR, it just passes the error through.
($ first-step (safe-divide 100 2)) ;; -> (result:ok 50)
($ second-step (result:then first-step (λ (value) (safe-divide value 5)))) ;; -> (result:ok 10)
($ final-result (result:then second-step (λ (value) (safe-divide value 10)))) ;; -> (result:ok 1)
;; Now, let's introduce an error.
($ failing-step (result:then first-step (λ (value) (safe-divide value 0)))) ;; -> (result:err "Division by zero...")
($ final-result-with-error (result:then failing-step (λ (value) (safe-divide value 2))))
;; The last step is never executed. The final result is still the first error.
;; final-result-with-error is (result:err "Division by zero...")
This demonstrates how Result
can create clean, resilient data processing pipelines where error handling is built-in, not an afterthought.
The Option
Type (Handling Absence)
Sometimes, the absence of a value isn't an "error" but an expected, valid outcome. For these cases, the Option
type is used. It represents either the presence or absence of a value.
(option:some value)
: Contains a value. For example,(option:some 42)
.(option:none)
: Represents the absence of a value. This is conceptually similar toNULL_SIG
but used explicitly within theOption
type system for clarity.
Example: A Safe Map Lookup
($ my-config (# :host "topaz.ooo" :port 80))
;; A safe map lookup that might return an Option type.
($ get-port (map:get my-config :port)) ;; Evaluates to a conceptual (option:some 80)
($ get-user (map:get my-config :user)) ;; Evaluates to a conceptual (option:none)
(IF (option:is-some get-user)
(io:print "User found in config.")
(io:print "User not found in config.")
)
Note: option:some
, option:none
, option:is-some
, and map:get
are the Option
type's constructors and related helper functions.