Error Handling
GoLisp follows Go's error model: functions return errors as values. There's no try/catch.
Multiple Return Values
A function that can fail returns [result error]:
(defn divide [a float64 b float64] -> [float64 error]
(if (= b 0.0)
(values 0.0 (error "division by zero"))
(values (/ a b) nil)))
if-err
if-err is the idiomatic way to check errors. It binds both return values and branches on whether the error is non-nil:
(if-err [result err] (divide 10.0 3.0)
(println "failed:" err) ; error branch
(println "result:" result)) ; success branch
The error branch runs first — this mirrors the Go convention of handling errors before using values.
Nested operations read naturally:
(defn read-json [path string] -> [any error]
(if-err [content err] (read-file path)
(values nil err)
(if-err [data err] (json/decode content)
(values nil err)
(values data nil))))
Creating Errors
error creates an error from a string:
(error "something went wrong")
wrap-error wraps an existing error with additional context:
(wrap-error "failed to load config" err)
Check if an error matches a specific type:
(errors/is? err io/EOF)
let-or — Sequential Nil Guard
let-or chains operations where each step can fail. If any binding is nil, it short-circuits to the fallback:
(defn process-request [req any] -> any
(let-or
[body (web/body-map req) {"error" "missing body"}
title (get body "title") {"error" "missing title"}
user (get body "user") {"error" "missing user"}]
{:title title :user user}))
Each binding has three parts: name, expression, fallback. The fallback is returned as soon as a binding resolves to nil.
Panic and Recover
For truly unrecoverable situations, panic stops the program:
(assert (> n 0) "n must be positive")
(panic "unreachable state")
recover can catch a panic inside a deferred function:
(defn safe-call [f any] -> any
(defer (fn [] -> void
(let [r (recover)]
(when r (println "caught panic:" r)))))
(f))
Panics are rare. Most failures should return an error value.
Practical Pattern
A typical function that reads, parses, and uses data:
(defn load-config [path string] -> [any error]
(if-err [raw err] (read-file path)
(values nil (wrap-error "read failed" err))
(if-err [cfg err] (json/decode raw)
(values nil (wrap-error "parse failed" err))
(values cfg nil))))
(defn main [] -> void
(if-err [cfg err] (load-config "config.json")
(do (println "error:" err) (sys/exit 1))
(println "loaded:" cfg)))