Error Handling Options

May 2016, Clojure Ireland Meetup

Errors will happen - have to handle the sad path

Dealing with errors

Available options

  • Crash
  • Throw exception
  • Recover, keep going
  • Return special value

The problem with try catch


(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"))
	      

Condition Systems

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.

Using Swell and Slingshot to define restarts


(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")))
           

Using Swell and Slingshot



;;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?)


	      

Dynamic binding


            ;; ==== 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?)
 

Side effects

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.

  • nil
  • Maybe
  • Either
  • Validation

Either monad


(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)))
	    

Validation applicative


(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)))

	    

Thank you!

Questions?