Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 18 additions & 13 deletions src/clj_commons/slingshot/support.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions test/clj_commons/slingshot_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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?"))))