├── CHANGELOG.md ├── README.md ├── deps.edn ├── project.clj ├── src └── clj │ └── qbits │ └── ex.clj └── test └── qbits └── ex └── test └── core_test.clj /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ex 2 | 3 | ## Deprecation notice 4 | 5 | **No longer in developement, if you want the same ideas with way better implementation, head to [exoscale/ex](https://github.com/exoscale/ex/)** 6 | 7 | 8 | 9 | [![cljdoc badge](https://cljdoc.xyz/badge/cc.qbits/ex)](https://cljdoc.xyz/d/cc.qbits/ex/CURRENT) 10 | 11 | An exception library, drop in replacement for `try`/`catch`/`finally`, 12 | that adds support for `ex-info`/`ex-data` with a custom (clojure) 13 | hierarchy that allows to express exceptions relations. 14 | 15 | So we have `qbits.ex/try+`, which supports vanilla `catch`/`finally` 16 | clauses. 17 | If you specify a `catch-data` clause with a keyword as first argument 18 | things get interesting. We assume you always put a `:type` key in the 19 | ex-info you want to use with this, and will match its value to the 20 | value of the key in the `catch-data` clause. 21 | 22 | Essentially `catch-data` takes this form: 23 | 24 | ``` clj 25 | (catch-data :something m 26 | ;; where m is a binding to the ex-data (you can destructure at that level as well) 27 | ) 28 | ``` 29 | 30 | So you can do things like that. 31 | 32 | ``` clj 33 | 34 | (require '[qbits.ex :as ex]) 35 | 36 | (ex/try+ 37 | 38 | (throw (ex-info "Argh" {:type ::bar :foo "a foo"})) 39 | 40 | (catch-data ::foo data 41 | (prn :got-ex-data data)) 42 | 43 | (catch-data ::bar {:as data :keys [foo]} 44 | ;; in that case it would hit this one 45 | (prn :got-ex-data-again foo)) 46 | 47 | (catch ExceptionInfo e 48 | ;; this would match an ex-info that didn't get a hit with catch-ex-info) 49 | 50 | (catch Exception e (prn :boring)) 51 | 52 | (finally (prn :boring-too))) 53 | 54 | ``` 55 | 56 | 57 | But there's a twist. 58 | 59 | I thought leveraging a clojure hierarchy could make sense in that 60 | context too, so you can essentially create exceptions hierachies 61 | without having to mess with Java classes directly and in a 62 | clojuresque" way. 63 | 64 | ``` clj 65 | ;; so bar is a foo 66 | 67 | (ex/derive ::bar ::foo) 68 | 69 | (ex/try+ 70 | (throw (ex-info "I am a bar" {:type ::bar}) 71 | (catch-data ::foo d 72 | (prn "got a foo with data" d) 73 | (prn "Original exception instance is " (-> d meta ::ex/exception)))) 74 | 75 | ``` 76 | 77 | You can also get the full exception instance via the metadata on the 78 | ex-data we extract, it's under the `:qbits.ex/exception` key. 79 | 80 | Some real life examples of usage for this: 81 | 82 | * make some exceptions end-user exposable in http responses via an 83 | error middleware in a declarative way . 84 | 85 | * skip sentry logging for some kind of exceptions (or the inverse) 86 | 87 | * make an exception hierachy for our query language type of errors for 88 | specialized reporting per "type" 89 | 90 | Other than that it's largely inspired by 91 | [catch-data](https://github.com/gfredericks/catch-data), the 92 | implementation is slightly different, we dont catch Throwable, we 93 | instead generate a catch clause on clj `ex-info` and generate a cond 94 | that tries to match ex-data with the :type key using `isa?` with our 95 | hierarchy, which arguably is closer to I would write by hand in that 96 | case. 97 | 98 | ## Installation 99 | 100 | ex is [available on Clojars](https://clojars.org/cc.qbits/ex). 101 | 102 | Add this to your dependencies: 103 | 104 | 105 | [![Clojars Project](https://img.shields.io/clojars/v/cc.qbits/ex.svg)](https://clojars.org/cc.qbits/ex) 106 | 107 | 108 | or you can just grab it via `deps.edn` directly 109 | 110 | 111 | 112 | 113 | 114 | ## License 115 | 116 | Copyright © 2018 [Max Penet](http://twitter.com/mpenet) 117 | 118 | Distributed under the 119 | [Eclipse Public License](http://www.eclipse.org/legal/epl-v10.html), 120 | the same as Clojure. 121 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.9.0"}} 2 | :paths ["src/clj"] 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git" 5 | :sha "5fb4fc46ad0bf2e0ce45eba5b9117a2e89166479"}} 6 | :main-opts ["-m" "cognitect.test-runner"]}}} 7 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject cc.qbits/ex "0.1.3" 2 | :description "Yet another exception catching library" 3 | :url "https://github.com/mpenet/ex" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.9.0"]] 7 | :source-paths ["src/clj"] 8 | :global-vars {*warn-on-reflection* true}) 9 | -------------------------------------------------------------------------------- /src/clj/qbits/ex.clj: -------------------------------------------------------------------------------- 1 | (ns qbits.ex 2 | (:refer-clojure :exclude [derive underive ancestors descendants isa? parents])) 3 | 4 | (defonce hierarchy (atom (make-hierarchy))) 5 | 6 | (defn derive 7 | "Like clojure.core/derive but scoped on our ex-info type hierarchy" 8 | [tag parent] 9 | (swap! hierarchy 10 | clojure.core/derive tag parent)) 11 | 12 | (defn underive 13 | "Like clojure.core/underive but scoped on our ex-info type hierarchy" 14 | [tag parent] 15 | (swap! hierarchy 16 | clojure.core/underive tag parent)) 17 | 18 | (defn ancestors 19 | "Like clojure.core/ancestors but scoped on our ex-info type hierarchy" 20 | [tag] 21 | (clojure.core/ancestors @hierarchy tag)) 22 | 23 | (defn descendants 24 | "Like clojure.core/descendants but scoped on our ex-info type hierarchy" 25 | [tag] 26 | (clojure.core/descendants @hierarchy tag)) 27 | 28 | (defn parents 29 | "Like clojure.core/parents but scoped on our ex-info type hierarchy" 30 | [tag] 31 | (clojure.core/parents @hierarchy tag)) 32 | 33 | (defn isa? 34 | "Like clojure.core/isa? but scoped on our ex-info type hierarchy" 35 | [child parent] 36 | (clojure.core/isa? @hierarchy child parent)) 37 | 38 | (defn find-clause-fn 39 | [pred] 40 | (fn [x] 41 | (and (seq? x) 42 | (pred (first x))))) 43 | 44 | (def catch-clause? (find-clause-fn #{'catch 'finally})) 45 | (def catch-data-clause? (find-clause-fn #{'catch-data})) 46 | 47 | (defmacro try+ 48 | "Like try but with support for ex-info/ex-data. 49 | 50 | If you pass a `catch-ex-info` form it will try to match an 51 | ex-info :type key, or it's potential ancestors in the local hierarchy. 52 | 53 | ex-info clauses are checked first, in the order they were specified. 54 | catch-ex-info will take as arguments a :type key, and a binding for 55 | the ex-data of the ex-info instance. 56 | 57 | (try 58 | [...] 59 | (catch-ex-info ::something my-ex-data 60 | (do-something my-ex-info)) 61 | (catch-ex-info ::something-else {:as my-ex-data :keys [foo bar]} 62 | (do-something foo bar)) 63 | (catch Exception e 64 | (do-something e)) 65 | (catch OtherException e 66 | (do-something e)) 67 | (finally :and-done)) 68 | 69 | You can specify normal catch clauses for regular java errors and/or 70 | finally these are left untouched." 71 | {:style/indent 2} 72 | [& xs] 73 | (let [[body mixed-clauses] 74 | (split-with (complement (some-fn catch-clause? catch-data-clause?)) 75 | xs) 76 | clauses (filter catch-clause? mixed-clauses) 77 | ex-info-clauses (filter catch-data-clause? mixed-clauses) 78 | type-sym (gensym "ex-type-") 79 | data-sym (gensym "ex-data-")] 80 | `(try 81 | ~@body 82 | ~@(cond-> clauses 83 | (seq ex-info-clauses) 84 | (conj `(catch clojure.lang.ExceptionInfo e# 85 | (let [~data-sym (vary-meta (ex-data e#) 86 | assoc ::exception e#) 87 | ~type-sym (:type ~data-sym)] 88 | (cond 89 | ~@(mapcat (fn [[_ type binding & body]] 90 | `[(isa? ~type-sym ~type) 91 | (let [~binding ~data-sym] ~@body)]) 92 | ex-info-clauses) 93 | :else 94 | ;; rethrow ex-info with other clauses since we 95 | ;; have no match 96 | (try (throw e#) 97 | ~@clauses))))))))) 98 | -------------------------------------------------------------------------------- /test/qbits/ex/test/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns qbits.ex.test.core-test 2 | (:use clojure.test) 3 | (:require [qbits.ex :as ex])) 4 | 5 | (defmacro try-val 6 | [& body] 7 | `(try 8 | ~@body 9 | (catch Exception e# 10 | e#))) 11 | 12 | (deftest test-foo 13 | (let [d {:type :foo}] 14 | ;; match 15 | (is (= d 16 | (ex/try+ 17 | (throw (ex-info "asdf" d)) 18 | (catch-data :foo x 19 | x)))) 20 | 21 | (is (true? 22 | (ex/try+ 23 | true 24 | (catch-data :foo x 25 | x)))) 26 | 27 | ;; no match but still ex-info 28 | (is (= {:type :asdf} 29 | (ex-data (try-val 30 | (ex/try+ 31 | (throw (ex-info "asdf" {:type :asdf})) 32 | (catch-data :foo x 33 | x)))))) 34 | 35 | (is (instance? Exception 36 | (try-val 37 | (ex/try+ 38 | (throw (Exception. "boom")) 39 | (catch-data :foo x 40 | x))))))) 41 | 42 | (deftest test-inheritance 43 | (ex/derive :bar :foo) 44 | (let [d {:type :bar}] 45 | (is (-> (ex/try+ 46 | (throw (ex-info "" d)) 47 | (catch-data :foo ex 48 | (= ex d)))))) 49 | (ex/derive :baz :bar) 50 | (let [e {:type :baz}] 51 | (is (-> (ex/try+ 52 | (throw (ex-info "" e)) 53 | (catch-data :foo ex 54 | (= ex e)))))) 55 | 56 | (let [e {:type :bak}] 57 | (is (-> (try-val (ex/try+ 58 | (throw (ex-info "" e)) 59 | (catch-data :foo ex 60 | (= e ex)))))))) 61 | 62 | (deftest test-bindings 63 | (is (ex/try+ 64 | (throw (ex-info "" {:type :foo 65 | :bar 1})) 66 | (catch-data :foo {:keys [bar]} 67 | (= bar 1))))) 68 | 69 | ;; (prn (ex-info "" {:type :ex-with-meta})) 70 | 71 | (deftest complex-meta 72 | (let [x (ex-info "" {:type :ex-with-meta})] 73 | (is (ex/try+ 74 | (throw x) 75 | (catch-data :ex-with-meta 76 | x' 77 | (-> x' meta ::ex/exception (= x))))))) 78 | 79 | ;; (run-tests) 80 | --------------------------------------------------------------------------------