How Phel signals and recovers from failures: throw to raise, try/catch/finally to handle, and ex-info to carry structured data with an error. Phel uses PHP's exception machinery, so any PHP Throwable works here.
Throwing#
(throw expr)
throw evaluates expr and throws it. The value must implement PHP's Throwable (every PHP exception does).
(throw (php/new \Exception "Something went wrong"))
;; Shorthand for constructing a class: (Class. args)
(throw (Exception. "Something went wrong"))Try, catch, finally#
(try expr* catch-clause* finally-clause?)
try evaluates its body. If nothing throws, it returns the last value. If a catch clause matches the thrown type, it returns that clause's value. A finally clause always runs last, for cleanup.
(try
(throw (Exception. "boom"))
(catch \Exception e "recovered")) ; => "recovered"
(try
(+ 1 1)
(finally (print "cleanup"))) ; => 2, and prints "cleanup"
(try
(throw (Exception. "boom"))
(catch \Exception e "recovered")
(finally (print "cleanup"))) ; => "recovered", and prints "cleanup"
A catch clause names the exception type and a symbol bound to the caught value. List several clauses to handle types differently; the first matching one wins.
(try
(throw (php/new \InvalidArgumentException "bad input"))
(catch \InvalidArgumentException e (str "arg error: " (php/-> e (getMessage))))
(catch \Exception e "other error"))
; => "arg error: bad input"Catching PHP exceptions#
Anything PHP can throw, you can catch. Reference the PHP class with a leading backslash (\Exception, \RuntimeException, \TypeError). Read its details with PHP method calls via php/->.
(try
(throw (php/new \RuntimeException "disk full"))
(catch \Exception e
(php/-> e (getMessage)))) ; => "disk full"
php/-> is the PHP method-call operator: (php/-> e (getMessage)) is the same as $e->getMessage() in PHP. Use it to reach getCode, getFile, getLine, and friends.
PHP Coming from PHP? ›
Same exceptions, different shape:
// PHP
try {
throw new \RuntimeException("disk full");
} catch (\Exception $e) {
echo $e->getMessage();
};; Phel
(try
(throw (php/new \RuntimeException "disk full"))
(catch \Exception e (php/-> e (getMessage))))
Structured errors with ex-info#
A plain message is often not enough. ex-info builds an exception that carries a data map (and an optional cause), so handlers can branch on machine-readable context instead of parsing strings.
(ex-info message data)
(ex-info message data cause)
(throw (ex-info "User not found" {:user-id 42 :status 404}))
Read the parts back with ex-message, ex-data, and ex-cause:
(def err (ex-info "Validation failed" {:field :email :reason "invalid format"}))
(ex-message err) ; => "Validation failed"
(ex-data err) ; => {:field :email :reason "invalid format"}
(ex-cause err) ; => nil (no cause provided)Branching on data#
(try
(throw (ex-info "User not found" {:status 404}))
(catch \Exception e
(case (:status (ex-data e))
404 "not found"
403 "forbidden"
"unknown error"))) ; => "not found"Chaining a cause#
Pass the original exception as the third argument to keep the failure trail. Read it back with ex-cause.
(try
(try
(throw (php/new \Exception "io fail"))
(catch \Exception e
(throw (ex-info "save failed" {:op :save} e))))
(catch \Exception e
(str (ex-message e) " <- " (ex-message (ex-cause e)))))
; => "save failed <- io fail"Clojure Coming from Clojure? ›
ex-info, ex-data, ex-message, and ex-cause work as in Clojure. The underlying object is a PHP exception, so catch \Exception also catches ex-info values.
When to throw vs return nil#
Throwing is for genuinely exceptional situations. For ordinary "no result" cases, returning nil is often cleaner and lets callers use if-let, when-let, or a default.
- Return
nilwhen absence is expected and the caller can handle it: a lookup miss, an empty parse, an optional field. - Throw when continuing would be a bug or the caller cannot reasonably proceed: invalid arguments, broken invariants, failed I/O.
;; Expected miss: return nil, let the caller decide
(defn find-user [users id]
(get users id)) ; nil when not present
;; Real failure: throw with context
(defn charge-card [amount]
(when (<= amount 0)
(throw (ex-info "Invalid charge amount" {:amount amount})))
amount)
(find-user {} 42) ; => nil
(charge-card 10) ; => 10Next steps#
- Control flow -
if,cond, andcasefor handling results - Basic types - why only
falseandnilare falsy - Cheat sheet - keep it open while coding