diff --git a/CHANGELOG.md b/CHANGELOG.md index bb5d01c..9690c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ * v0.13.next in progress * address [clj-commons/slingshot#8](https://github.com/clj-commons/slingshot/issues/8) by removing outdated status note. + * address [clj-commons/slingshot#5](https://github.com/clj-commons/slingshot/issues/5) by attempting to catch the wrapper (exception) type if no catch clause matches the wrapped object type. * address [clj-commons/slingshot#4](https://github.com/clj-commons/slingshot/issues/4) by merging stacktrace changes from [a20748f](https://github.com/clj-commons/slingshot/commit/a20748fd2d6d4a9020d296a3798e82f136e2bfe2) by [@scgilardi](https://github.com/scgilardi). * address [clj-commons/slingshot#3](https://github.com/clj-commons/slingshot/issues/3) by merging `and` optimizations from original PR [#55](https://github.com/scgilardi/slingshot/pull/55) by [@jbouwman](https://github.com/jbouwman). * address [scgilardo/slingshot#53](https://github.com/scgilardi/slingshot/issues/53) by adding examples of `else` and `finally`. + * update dev/test/build deps * v0.13.0 -- 2025-09-20 - add clj-kondo config export diff --git a/README.md b/README.md index 1c016b6..3d5940f 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,41 @@ caught {:value 5} {:object {:value 5}, :message important message, :cause nil, :throwable #error {...}} ``` +The above example shows that if you `catch Object`, you can catch non-Throwable +data (as well as wrapped exceptions) and access the full throw context. + +You can also catch based on the wrapper: + +```clojure +user=> (defn foo [x] + #_=> (throw (ex-info "important message" {:value x}))) + #_=> + #_=> (defn -main [] + #_=> (try+ + #_=> (foo 5) + #_=> (catch Exception e + #_=> (println "caught" e &throw-context)))) +#'user/foo +#'user/-main +user=> (-main) +caught #error { + :cause important message + :data {:value 5} + :via + [{:type clojure.lang.ExceptionInfo + :message important message + :data {:value 5} + :at [...]}] + :trace + [[...]]} + {:object {:value 5}, :message important message, :cause nil, + :stack-trace #object[[Ljava.lang.StackTraceElement...], + :wrapper #error { + :cause important message + ...}, + :throwable #error {...}} +``` + Between being thrown and caught, a wrapper may be further wrapped by other Exceptions (e.g., instances of `RuntimeException` or `java.util.concurrent.ExecutionException`). `try+` sees through all diff --git a/src/clj_commons/slingshot/support.clj b/src/clj_commons/slingshot/support.clj index 4d07e07..5d94819 100644 --- a/src/clj_commons/slingshot/support.clj +++ b/src/clj_commons/slingshot/support.clj @@ -153,28 +153,33 @@ `(~selector ~'%))] `(let [~'% (:object ~'&throw-context)] ~(or (key-values) (selector-form) (predicate))))) - (cond-expression [binding-form expressions] - `(let [~binding-form (:object ~'&throw-context)] + (cond-expression [k binding-form expressions] + `(let [~binding-form (~k ~'&throw-context)] ~@expressions)) (transform [[_ selector binding-form & expressions]] (if-let [class-selector (class-selector? selector)] [`(instance? ~class-selector (:object ~'&throw-context)) - (cond-expression (with-meta binding-form {:tag selector}) expressions)] - [(cond-test selector) (cond-expression binding-form expressions)]))] + (cond-expression :object (with-meta binding-form {:tag selector}) expressions)] + [(cond-test selector) (cond-expression :object binding-form expressions)])) + (transform-wrapper [[_ selector binding-form & expressions]] + (when-let [class-selector (class-selector? selector)] + [`(instance? ~class-selector (:wrapper ~'&throw-context)) + (cond-expression :wrapper (with-meta binding-form {:tag selector}) expressions)]))] (list `(catch Throwable ~'&throw-context (reset! ~threw?-sym true) (let [~'&throw-context (-> ~'&throw-context get-context *catch-hook*)] (cond - (contains? ~'&throw-context :catch-hook-return) - (:catch-hook-return ~'&throw-context) - (contains? ~'&throw-context :catch-hook-throw) - (~throw-sym (:catch-hook-throw ~'&throw-context)) - (contains? ~'&throw-context :catch-hook-rethrow) - (~throw-sym) - ~@(mapcat transform catch-clauses) - :else - (~throw-sym))))))) + (contains? ~'&throw-context :catch-hook-return) + (:catch-hook-return ~'&throw-context) + (contains? ~'&throw-context :catch-hook-throw) + (~throw-sym (:catch-hook-throw ~'&throw-context)) + (contains? ~'&throw-context :catch-hook-rethrow) + (~throw-sym) + ~@(mapcat transform catch-clauses) + ~@(mapcat transform-wrapper catch-clauses) + :else + (~throw-sym))))))) (defn gen-finally "Returns either nil or a list containing a finally clause for a try diff --git a/test/clj_commons/slingshot_test.clj b/test/clj_commons/slingshot_test.clj index 8eae464..27b4096 100644 --- a/test/clj_commons/slingshot_test.clj +++ b/test/clj_commons/slingshot_test.clj @@ -518,3 +518,50 @@ (is (= result23 [exp msg])) (is (= result24 [exp fmt-msg])) (is (= result25 [exp fmt2-msg])))))) + +(deftest test-catch-exception + (testing "throw+ Exception" + (let [caught (atom false)] + (try+ + (throw+ (Exception. "test-exception")) + (catch Exception e + (reset! caught true) + (is (= "test-exception" (.getMessage e))))) + (is @caught "exception was not caught?")) + (let [caught (atom false)] + (try+ + (throw+ (Exception. "test-exception")) + (catch Object e + (reset! caught true) + (is (= "test-exception" (.getMessage e))))) + (is @caught "exception was not caught?"))) + (testing "throw Exception" + (let [caught (atom false)] + (try+ + (throw (Exception. "test-exception")) + (catch Exception e + (reset! caught true) + (is (= "test-exception" (.getMessage e))))) + (is @caught "exception was not caught?")) + (let [caught (atom false)] + (try+ + (throw (Exception. "test-exception")) + (catch Object e + (reset! caught true) + (is (= "test-exception" (.getMessage e))))) + (is @caught "exception was not caught?"))) + (testing "throw data" + (let [caught (atom false)] + (try+ + (throw+ {:msg "test-exception"}) + (catch Exception e + (reset! caught true) + (is (= "test-exception" (:msg (ex-data e)))))) + (is @caught "exception was not caught?")) + (let [caught (atom false)] + (try+ + (throw+ {:msg "test-exception"}) + (catch Object e + (reset! caught true) + (is (= "test-exception" (:msg e))))) + (is @caught "exception was not caught?"))))