catch exceptions hook, line, and sinker

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

+43 -19
+22 -11
README.md
··· 13 13 it's `try` but a little nicer: 14 14 15 15 ```clojure 16 - (require '[noahtheduke.sinker :refer [try+]]) 16 + (require '[noahtheduke.sinker :as sinker :refer [try+]]) 17 17 18 18 ;; works like normal try 19 + (try+) 20 + ;; => nil 21 + 19 22 (try+ 1 2 3) 20 23 ;; => 3 21 24 ··· 24 27 (ex-message ex))) 25 28 ;; => "hello world!" 26 29 27 - ;; ex-infos with `:type` ex-data can be caught with a keyword. 30 + ;; ex-infos with `:noahtheduke.sinker/type` (or `:type`) ex-data can be caught with a keyword. 28 31 ;; the keywords are compared with isa? to respect heirarchies. 29 32 ;; the bound variable is the ex-data, not the exception itself. 30 - (try+ (throw (ex-info "Wrong parameter" {:type :invalid-parameter 33 + (try+ (throw (ex-info "Wrong parameter" {::sinker/type :invalid-parameter 31 34 :expected :abc 32 35 :given :foobar})) 33 36 (catch :invalid-parameter data ··· 35 38 ;; => :foobar 36 39 37 40 ;; 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 41 + (try+ (throw (ex-info "Wrong parameter" {::sinker/type :invalid-parameter 39 42 :expected 'abc 40 43 :given 'foobar})) 41 44 (catch :invalid-parameter data ··· 43 46 ;; => "Wrong parameter" 44 47 45 48 ;; because the ex-data is a map, it can be destructured 46 - (try+ (throw (ex-info "Wrong parameter" {:type :invalid-parameter 49 + (try+ (throw (ex-info "Wrong parameter" {::sinker/type :invalid-parameter 47 50 :expected :abc 48 51 :given :foobar})) 49 52 (catch :invalid-parameter {:keys [expected given]} ··· 56 59 (defn pred [data] 57 60 (= :value (:key data))) 58 61 59 - (try+ (throw (ex-info "KV pair" {:type :incorrect-argument 62 + (try+ (throw (ex-info "KV pair" {::sinker/type :incorrect-argument 60 63 :key :value})) 61 - (catch pred data 62 - (:key data))) 64 + (catch pred {k :key} 65 + k)) 63 66 ;; => :value 64 67 65 68 ;; like normal try, each catch is checked in definition order, 66 69 ;; and finally clauses gotta come last 67 70 (defn pred2 [data] 68 71 (= :value2 (:key2 data))) 69 - (def errored? (atom nil)) 72 + 73 + (def finally-ran? (atom nil)) 70 74 71 75 (try+ (assert (= 1 2) "This will work") 72 76 (catch :invalid-argument _ ··· 80 84 (catch Throwable t 81 85 (str "Received a " (.getName (class t)))) 82 86 (finally 83 - (reset! errored? "hoodee hoodee hoo"))) 87 + (reset! finally-ran? "hoodee hoodee hoo"))) 84 88 ;; => "Received a java.lang.AssertionError" 85 89 86 - @errored? 90 + @finally-ran? 87 91 ;; => "hoodee hoodee hoo" 88 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? 89 100 90 101 ## license 91 102
+1 -1
src/noahtheduke/sinker.clj
··· 58 58 [t ex-info? data catch-clauses] 59 59 (for [{:keys [type pred id body]} catch-clauses] 60 60 (case type 61 - :catch/type `[(and ~ex-info? (isa? ~pred (:type ~data))) 61 + :catch/type `[(and ~ex-info? (isa? ~pred (or (::type ~data) (:type ~data)))) 62 62 (let [data# (or (ex-data ~t) {}) 63 63 ~id (vary-meta data# assoc ::exception ~t)] 64 64 ~@body)]
+20 -7
test/noahtheduke/sinker_test.clj
··· 4 4 5 5 (ns noahtheduke.sinker-test 6 6 (:require 7 - [lazytest.core :refer [defdescribe describe expect-it causes-with-msg? it expect]] 8 - [noahtheduke.sinker :as sut]) 7 + [lazytest.core :refer [defdescribe describe expect-it causes-with-msg? it expect throws?]] 8 + [noahtheduke.sinker :as sut]) 9 9 (:import 10 10 [clojure.lang ExceptionInfo])) 11 11 ··· 24 24 ;; keyword 25 25 (catch ::special-exception data 26 26 [::special-exception data]) 27 + (catch ::another-exception {:keys [a b c]} 28 + [::another-exception a b c]) 27 29 ;; predicate 28 30 (catch pred data 29 31 [::pred data]) ··· 44 46 (expect-it "works like try when given multiple expressions" 45 47 #_{:clj-kondo/ignore [:missing-clause-in-try]} 46 48 (= 3 (sut/try+ 1 2 3))) 47 - (expect-it "uses keywords" 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" 48 53 (= [::special-exception {:type ::special-exception}] 49 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}))))) 50 59 (expect-it "uses predicates" 51 60 (= [::pred {:key :value}] 52 61 (test-try #(throw (ex-info "" {:key :value}))))) ··· 57 66 (let [[v ex] (test-try #(throw (IllegalArgumentException. "")))] 58 67 (expect (= ::class v)) 59 68 (expect (instance? IllegalArgumentException ex)))) 60 - (it "uses java classes" 69 + (it "uses ExceptionInfo" 61 70 (let [[v ex] (test-try #(throw (ex-info "" {:no :data})))] 62 71 (expect (= ::ex-info v)) 63 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. "")))))))) 64 77 (it "works with finally" 65 78 (let [flag (atom false)] 66 79 (expect (= 1 (sut/try+ 1 (finally (swap! flag not))))) ··· 68 81 (describe "operates in order" 69 82 (expect-it "to choose keyword over predicate" 70 83 (= ::special-exception 71 - (first (test-try #(throw (ex-info "" {:type ::special-exception 84 + (first (test-try #(throw (ex-info "" {::sut/type ::special-exception 72 85 :key :value})))))) 73 86 (expect-it "to choose keyword over predicate" 74 87 (= ::pred (first (test-try #(throw (ex-info "" {:key :value ··· 113 126 114 127 (defdescribe macro-test 115 128 (it "works with macro-expansion" 116 - (expect (= {:type ::macro-exception 129 + (expect (= {::sut/type ::macro-exception 117 130 :flavor :grape} 118 - (example-macro (throw (ex-info "gotcha" {:type ::macro-exception 131 + (example-macro (throw (ex-info "gotcha" {::sut/type ::macro-exception 119 132 :flavor :grape})))))))