On Github jcantwell / error-handling-talk
May 2016, Clojure Ireland Meetup
The outside world will always want to intrude into our applications. As much as possible we want to be resilient to failure or at least clearly communicate the error and the current state of the system.
Much of the content taken from a talk Chris Houser gave a Clojure/conj 2015. Try to quickly summarize the important points from that talk and also take a quick look at returning special values. The idomatic approach in clojure seems to be to use throw exceptions and use try catch. This works well for unrecoverable errors but not great if you want to recover from the error
(defn check-for-lucky-number [n] (if (#{4 14 24} n) n (throw (ex-info "unlucky number" {:number n})))) (try (mapv check-for-lucky-number (range 20)) (catch RuntimeException e "Not much we can do to recover here"))When you throw an exception you leave the current stack. This context is often needed to be able to recover. Matching on exception type hierarchy which feels a little clumsey.
Higher level functions specify what action to take in response to an error, the action implementation is provided by the calling function.
Errors are handled when and where they occur rather than catching an exception far away from the origin where we can't to do anything about it.
(ns error-handling-test.condition-system (:require [slingshot.core :refer [throw+]] [swell.api :refer [ restart-case handler-bind invoke-restart]])) (defn check-for-lucky-number [n] (if (#{4 14 24} n) n (restart-case [ :try-again (fn [value] (println "trying again with "value) (check-for-lucky-number value)) :treat-as-lucky (fn [] (println "treating the value as lucky") n) :skip (fn [] (println "Returning dash instead") "-") ] (throw+ {:type ::unlucky-number :number n} "Landed on unlucky number")) )) (defn lucky-number? [n] (handler-bind [#(= ::unlucky-number (:type %)) (fn [e] ;;choose the restart appropriate for the error ;; could also rethrow error (invoke-restart :try-again (rand-int 10) ))] (if (check-for-lucky-number n) "YES")))
;;Instead of an error being raised the ;;check-for-lucky-number function is called ;;with different values until it finds a lucky number (lucky-number? 16) ;;Without binding a restart all we just know ;;something went wrong (try (mapv check-for-lucky-number (range 20)) (catch RuntimeException e "Unlucky number in the sequence")) ;;Using restarts we can handle the error ;;appropriately at the point it occurred (defn lucky-numbers? [] (handler-bind [#(= ::unlucky-number (:type %)) (fn [e] (invoke-restart :skip ))] (mapv check-for-lucky-number (range 20)))) (lucky-numbers?)
There are a number of different condition system libraries. error-kit slingshot and swell ribol (now part of hara) bwo/conditions The fact that it is not part of the language means that if you are building a library you can assume the client code will be using the same condition system library.
;; ==== errors === (defn ^:dynamic *unlucky-number-error* [msg info] (throw (ex-info msg info))) ;; === restart === (defn ^:dynamic *treat-as-lucky* [value] (throw (ex-info "Restart *treat-as-lucky* is unbound."))) (defn ^:dynamic *try-again* [value] (throw (ex-info "Restart *try-again* is unbound."))) (defn ^:dynamic *skip* [value] (throw (ex-info "Restart *skip* is unbound."))) (defn check-for-lucky-number [n] (if (#{4 13 14 24} n) n (binding [*treat-as-lucky* identity *try-again* (fn [value] (println "trying again with "value) (check-for-lucky-number value)) *skip* (fn [] "-")] (*unlucky-number-error* "Landed on unlucky number" {:number n})))) (defn am-I-lucky? [n] ;;outer function chooses what restart to call based on the error (binding [*unlucky-number-error* (fn [msg info] (*try-again* (rand-int 10)))] (if (check-for-lucky-number n) "YES"))) (am-I-lucky? 41) ;;Without binding a restart all we just know something went wrong (try (mapv check-for-lucky-number (range 20)) (catch RuntimeException e "Unlucky number in the sequence")) ;;Using restarts we can handle the error ;;appropriately at the point it occurred (defn lucky-numbers? [] (binding [*unlucky-number-error* (fn [msg info] (*skip*))] (mapv check-for-lucky-number (range 20)))) (lucky-numbers?)
Exceptions are side effects. Makes it difficult to compose functions when you have a backchannel result. Using special return values makes the error conditions we expect to handle more explicit.
(ns error-handling-test.monads (require [cats.core :as m]) (require [cats.builtin]) (require [cats.monad.either :as either]) (require [cats.monad.maybe :as maybe])) (defn value-set [value] (if (nil? value) (either/left "Required value") (either/right value))) (defn valid-email [value] (if (re-matches #"\S+@\S+\.\S+" value) (either/right value) (either/left "invalid email"))) (defn valid-zip-code [zipCode] (if (re-matches #"\d{5}" zipCode) (either/right zipCode) (either/left "invalid zip code"))) (let [contact {:name "batman" :email "batman99@gmail.com" :zipCode "12345"}] (pr-str (m/mlet [ valueSet (value-set (:name contact)) email (valid-email (:email contact)) zip (valid-zip-code (:zipCode contact))] contact))) (let [contact {:name nil :email "batman99@gmail.com" :zipCode "12345678"}] (pr-str (m/mlet [ valueSet (value-set (:name contact)) email (valid-email (:email contact)) zip (valid-zip-code (:zipCode contact))] contact)))
Maybe: represents the posible absence of a value. Similar behavior that we get from Maybe can be achieved through nil, eg. theading can shortcircuit using something Either: represents a succcesful or errorful value cats is a library of category theory and algebraic abstractions for Clojure and ClojureScript. mlet - similar to for but can work with arbitary monads rather that just lists.
(ns error-handling-test.monads (require [cats.core :as m]) (require [cats.builtin]) (require [cats.applicative.validation :as v])) (defn value-set [value] (if (nil? value) (v/fail {:required "Required value"}) (v/ok value))) (defn valid-email [value] (if (re-matches #"\S+@\S+\.\S+" value) (v/ok value) (v/fail {:email "invalid email"}))) (defn valid-zip-code [zipCode] (if (re-matches #"\d{5}" zipCode) (v/ok zipCode) (v/fail {:zipCode "invalid zip code"}))) (let [contact {:name "batman" :email "batman99@gmail.com" :zipCode "12345"}] (pr-str (m/alet [ valueSet (value-set (:name contact)) email (valid-email (:email contact)) zip (valid-zip-code (:zipCode contact))] contact))) ;;unlike Either, aggregates failure values (let [contact {:name nil :email "batman99@gmail.com" :zipCode "123456"}] (pr-str (m/alet [ valueSet (value-set (:name contact)) email (valid-email (:email contact)) zip (valid-zip-code (:zipCode contact))] contact)))
Questions?