catch exceptions hook, line, and sinker

support ::noahtheduke.sinker/type, add rationale to readme

+43 -19
+22 -11
README.md
··· 13 it's `try` but a little nicer: 14 15 ```clojure 16 - (require '[noahtheduke.sinker :refer [try+]]) 17 18 ;; works like normal try 19 (try+ 1 2 3) 20 ;; => 3 21 ··· 24 (ex-message ex))) 25 ;; => "hello world!" 26 27 - ;; ex-infos with `:type` ex-data can be caught with a keyword. 28 ;; the keywords are compared with isa? to respect heirarchies. 29 ;; the bound variable is the ex-data, not the exception itself. 30 - (try+ (throw (ex-info "Wrong parameter" {:type :invalid-parameter 31 :expected :abc 32 :given :foobar})) 33 (catch :invalid-parameter data ··· 35 ;; => :foobar 36 37 ;; the exception is on the metadata of the bind under the key `:noahtheduke.sinker/exception`. 38 - (try+ (throw (ex-info "Wrong parameter" {:type :invalid-parameter 39 :expected 'abc 40 :given 'foobar})) 41 (catch :invalid-parameter data ··· 43 ;; => "Wrong parameter" 44 45 ;; because the ex-data is a map, it can be destructured 46 - (try+ (throw (ex-info "Wrong parameter" {:type :invalid-parameter 47 :expected :abc 48 :given :foobar})) 49 (catch :invalid-parameter {:keys [expected given]} ··· 56 (defn pred [data] 57 (= :value (:key data))) 58 59 - (try+ (throw (ex-info "KV pair" {:type :incorrect-argument 60 :key :value})) 61 - (catch pred data 62 - (:key data))) 63 ;; => :value 64 65 ;; like normal try, each catch is checked in definition order, 66 ;; and finally clauses gotta come last 67 (defn pred2 [data] 68 (= :value2 (:key2 data))) 69 - (def errored? (atom nil)) 70 71 (try+ (assert (= 1 2) "This will work") 72 (catch :invalid-argument _ ··· 80 (catch Throwable t 81 (str "Received a " (.getName (class t)))) 82 (finally 83 - (reset! errored? "hoodee hoodee hoo"))) 84 ;; => "Received a java.lang.AssertionError" 85 86 - @errored? 87 ;; => "hoodee hoodee hoo" 88 ``` 89 90 ## license 91
··· 13 it's `try` but a little nicer: 14 15 ```clojure 16 + (require '[noahtheduke.sinker :as sinker :refer [try+]]) 17 18 ;; works like normal try 19 + (try+) 20 + ;; => nil 21 + 22 (try+ 1 2 3) 23 ;; => 3 24 ··· 27 (ex-message ex))) 28 ;; => "hello world!" 29 30 + ;; ex-infos with `:noahtheduke.sinker/type` (or `:type`) ex-data can be caught with a keyword. 31 ;; the keywords are compared with isa? to respect heirarchies. 32 ;; the bound variable is the ex-data, not the exception itself. 33 + (try+ (throw (ex-info "Wrong parameter" {::sinker/type :invalid-parameter 34 :expected :abc 35 :given :foobar})) 36 (catch :invalid-parameter data ··· 38 ;; => :foobar 39 40 ;; the exception is on the metadata of the bind under the key `:noahtheduke.sinker/exception`. 41 + (try+ (throw (ex-info "Wrong parameter" {::sinker/type :invalid-parameter 42 :expected 'abc 43 :given 'foobar})) 44 (catch :invalid-parameter data ··· 46 ;; => "Wrong parameter" 47 48 ;; because the ex-data is a map, it can be destructured 49 + (try+ (throw (ex-info "Wrong parameter" {::sinker/type :invalid-parameter 50 :expected :abc 51 :given :foobar})) 52 (catch :invalid-parameter {:keys [expected given]} ··· 59 (defn pred [data] 60 (= :value (:key data))) 61 62 + (try+ (throw (ex-info "KV pair" {::sinker/type :incorrect-argument 63 :key :value})) 64 + (catch pred {k :key} 65 + k)) 66 ;; => :value 67 68 ;; like normal try, each catch is checked in definition order, 69 ;; and finally clauses gotta come last 70 (defn pred2 [data] 71 (= :value2 (:key2 data))) 72 + 73 + (def finally-ran? (atom nil)) 74 75 (try+ (assert (= 1 2) "This will work") 76 (catch :invalid-argument _ ··· 84 (catch Throwable t 85 (str "Received a " (.getName (class t)))) 86 (finally 87 + (reset! finally-ran? "hoodee hoodee hoo"))) 88 ;; => "Received a java.lang.AssertionError" 89 90 + @finally-ran? 91 ;; => "hoodee hoodee hoo" 92 ``` 93 + 94 + ## others in the space 95 + 96 + - [exoscale/ex](https://github.com/exoscale/ex) 97 + - [scgilardi/slingshot](https://github.com/scgilardi/slingshot/) 98 + 99 + this library is quite similar to `exoscale/ex`, but `ex` is solely focused on exception infos and does a lot more, with a stronger emphasis on a specific pattern of error handling. i wrote this to fill a gap in [lazytest](https://github.com/NoahTheDuke/lazytest) and to satisfy my curiosity. i don't expect this to receive widespread adoption nor do i really want it. sometimes it's just nice to make something and let others check it out, you know? 100 101 ## license 102
+1 -1
src/noahtheduke/sinker.clj
··· 58 [t ex-info? data catch-clauses] 59 (for [{:keys [type pred id body]} catch-clauses] 60 (case type 61 - :catch/type `[(and ~ex-info? (isa? ~pred (:type ~data))) 62 (let [data# (or (ex-data ~t) {}) 63 ~id (vary-meta data# assoc ::exception ~t)] 64 ~@body)]
··· 58 [t ex-info? data catch-clauses] 59 (for [{:keys [type pred id body]} catch-clauses] 60 (case type 61 + :catch/type `[(and ~ex-info? (isa? ~pred (or (::type ~data) (:type ~data)))) 62 (let [data# (or (ex-data ~t) {}) 63 ~id (vary-meta data# assoc ::exception ~t)] 64 ~@body)]
+20 -7
test/noahtheduke/sinker_test.clj
··· 4 5 (ns noahtheduke.sinker-test 6 (:require 7 - [lazytest.core :refer [defdescribe describe expect-it causes-with-msg? it expect]] 8 - [noahtheduke.sinker :as sut]) 9 (:import 10 [clojure.lang ExceptionInfo])) 11 ··· 24 ;; keyword 25 (catch ::special-exception data 26 [::special-exception data]) 27 ;; predicate 28 (catch pred data 29 [::pred data]) ··· 44 (expect-it "works like try when given multiple expressions" 45 #_{:clj-kondo/ignore [:missing-clause-in-try]} 46 (= 3 (sut/try+ 1 2 3))) 47 - (expect-it "uses keywords" 48 (= [::special-exception {:type ::special-exception}] 49 (test-try #(throw (ex-info "" {:type ::special-exception}))))) 50 (expect-it "uses predicates" 51 (= [::pred {:key :value}] 52 (test-try #(throw (ex-info "" {:key :value}))))) ··· 57 (let [[v ex] (test-try #(throw (IllegalArgumentException. "")))] 58 (expect (= ::class v)) 59 (expect (instance? IllegalArgumentException ex)))) 60 - (it "uses java classes" 61 (let [[v ex] (test-try #(throw (ex-info "" {:no :data})))] 62 (expect (= ::ex-info v)) 63 (expect (instance? ExceptionInfo ex)))) 64 (it "works with finally" 65 (let [flag (atom false)] 66 (expect (= 1 (sut/try+ 1 (finally (swap! flag not))))) ··· 68 (describe "operates in order" 69 (expect-it "to choose keyword over predicate" 70 (= ::special-exception 71 - (first (test-try #(throw (ex-info "" {:type ::special-exception 72 :key :value})))))) 73 (expect-it "to choose keyword over predicate" 74 (= ::pred (first (test-try #(throw (ex-info "" {:key :value ··· 113 114 (defdescribe macro-test 115 (it "works with macro-expansion" 116 - (expect (= {:type ::macro-exception 117 :flavor :grape} 118 - (example-macro (throw (ex-info "gotcha" {:type ::macro-exception 119 :flavor :grape})))))))
··· 4 5 (ns noahtheduke.sinker-test 6 (:require 7 + [lazytest.core :refer [defdescribe describe expect-it causes-with-msg? it expect throws?]] 8 + [noahtheduke.sinker :as sut]) 9 (:import 10 [clojure.lang ExceptionInfo])) 11 ··· 24 ;; keyword 25 (catch ::special-exception data 26 [::special-exception data]) 27 + (catch ::another-exception {:keys [a b c]} 28 + [::another-exception a b c]) 29 ;; predicate 30 (catch pred data 31 [::pred data]) ··· 46 (expect-it "works like try when given multiple expressions" 47 #_{:clj-kondo/ignore [:missing-clause-in-try]} 48 (= 3 (sut/try+ 1 2 3))) 49 + (expect-it "uses keywords with :noahtheduke.sinker/type" 50 + (= [::special-exception {::sut/type ::special-exception}] 51 + (test-try #(throw (ex-info "" {::sut/type ::special-exception}))))) 52 + (expect-it "uses keywords with :type" 53 (= [::special-exception {:type ::special-exception}] 54 (test-try #(throw (ex-info "" {:type ::special-exception}))))) 55 + (expect-it "destructures the ex-data" 56 + (= [::another-exception 1 2 3] 57 + (test-try #(throw (ex-info "" {::sut/type ::another-exception 58 + :a 1 :b 2 :c 3}))))) 59 (expect-it "uses predicates" 60 (= [::pred {:key :value}] 61 (test-try #(throw (ex-info "" {:key :value}))))) ··· 66 (let [[v ex] (test-try #(throw (IllegalArgumentException. "")))] 67 (expect (= ::class v)) 68 (expect (instance? IllegalArgumentException ex)))) 69 + (it "uses ExceptionInfo" 70 (let [[v ex] (test-try #(throw (ex-info "" {:no :data})))] 71 (expect (= ::ex-info v)) 72 (expect (instance? ExceptionInfo ex)))) 73 + (it "acts like a normal try when no branch matches" 74 + (expect 75 + (throws? IllegalStateException 76 + (fn [] (test-try (fn [] (throw (IllegalStateException. "")))))))) 77 (it "works with finally" 78 (let [flag (atom false)] 79 (expect (= 1 (sut/try+ 1 (finally (swap! flag not))))) ··· 81 (describe "operates in order" 82 (expect-it "to choose keyword over predicate" 83 (= ::special-exception 84 + (first (test-try #(throw (ex-info "" {::sut/type ::special-exception 85 :key :value})))))) 86 (expect-it "to choose keyword over predicate" 87 (= ::pred (first (test-try #(throw (ex-info "" {:key :value ··· 126 127 (defdescribe macro-test 128 (it "works with macro-expansion" 129 + (expect (= {::sut/type ::macro-exception 130 :flavor :grape} 131 + (example-macro (throw (ex-info "gotcha" {::sut/type ::macro-exception 132 :flavor :grape})))))))