diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2166853f..5bff8cb7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@
 
 * Bump `cljfmt` to 0.9.2.
 * Bump `puget` to 1.3.4.
+* [#769](https://github.com/clojure-emacs/cider-nrepl/issues/769): Introduce new `#break!` and `#dbg!` reader macros for breaking on exception
+* [#772](https://github.com/clojure-emacs/cider-nrepl/issues/772): Improve debugger heuristics on "interesting" breakpoints (Always break on forms tagged with #break, including vectors and maps)
+* The `wrap-debug` middleware now adds an optional `:caught-msg` key to the `eval` op response, containing the exception message when caught by the debugger. 
 
 ## 0.30.0 (2023-01-31)
 
diff --git a/doc/modules/ROOT/pages/hacking.adoc b/doc/modules/ROOT/pages/hacking.adoc
index 30471363..b0867554 100644
--- a/doc/modules/ROOT/pages/hacking.adoc
+++ b/doc/modules/ROOT/pages/hacking.adoc
@@ -1,12 +1,12 @@
 = Hacking on cider-nrepl
 
-Hacking on cider-nrepl requires nothing bit a bit of knowledge of Clojure and nREPL.
+Hacking on cider-nrepl requires nothing but a bit of knowledge of Clojure and nREPL.
 In this section we'll take a look at some practical tips to make you more productive
 while working on the project.
 
 == Makefile
 
-cider-nrepl has some pretty complicated Lein profiles, as it has to deal with multiple version of
+cider-nrepl has some pretty complicated Lein profiles, as it has to deal with multiple versions of
 Clojure and ClojureScript, plus dependency inlining with Mr. Anderson. That's why we've
 added a good old `Makefile` to save you the trouble of having to think about the profiles
 and just focus on the tasks at hand.
diff --git a/src/cider/nrepl/middleware/debug.clj b/src/cider/nrepl/middleware/debug.clj
index ad51742d..2c7605ec 100644
--- a/src/cider/nrepl/middleware/debug.clj
+++ b/src/cider/nrepl/middleware/debug.clj
@@ -284,15 +284,18 @@ this map (identified by a key), and will `dissoc` it afterwards."}
          :rendered pr-str)))
 
 (defn- debug-stacktrace
-  "Create a dummy exception, send its stack."
-  []
+  "Send the stacktrace of `value` if it is an exception.
+  Otherwise, create a dummy exception to view the call stack at the current location."
+  [value]
   (debugger-send
    {:status :stack
-    :causes [{:class "StackTrace"
-              :message "Harmless user-requested stacktrace"
-              :stacktrace (-> (Exception. "Dummy")
-                              (stacktrace.analyzer/analyze (::print/print-fn *msg*))
-                              last :stacktrace)}]}))
+    :causes (if (instance? Throwable value)
+              (stacktrace.analyzer/analyze value (::print/print-fn *msg*))
+              [{:class      "StackTrace"
+                :message    "Harmless user-requested stacktrace"
+                :stacktrace (-> (Exception. "Dummy")
+                                (stacktrace.analyzer/analyze (::print/print-fn *msg*))
+                                last :stacktrace)}])}))
 
 (def debug-commands
   "An unsorted set of commands supported by the debugger."
@@ -365,7 +368,7 @@ this map (identified by a key), and will `dissoc` it afterwards."}
                         value)
         :here       (do (skip-breaks! :before coord (:code dbg-state) force?)
                         value)
-        :stacktrace (do (debug-stacktrace)
+        :stacktrace (do (debug-stacktrace value)
                         (recur value dbg-state))
         :trace      (do (skip-breaks! :trace)
                         value)
@@ -488,6 +491,13 @@ this map (identified by a key), and will `dissoc` it afterwards."}
      ~@body))
 
 (defmacro breakpoint-with-initial-debug-bindings
+  {:style/indent 1}
+  [form dbg-state original-form]
+  `(with-initial-debug-bindings
+     (breakpoint
+      ~form ~dbg-state ~original-form)))
+
+(defmacro breakpoint-if-interesting-with-initial-debug-bindings
   {:style/indent 1}
   [form dbg-state original-form]
   `(with-initial-debug-bindings
@@ -568,48 +578,48 @@ this map (identified by a key), and will `dissoc` it afterwards."}
       (and (seq? form)
            (irrelevant-return-value-forms (first form)))))
 
+(defmacro breakpoint
+  "Wrap form in a breakpoint unconditionally."
+  [form {:keys [coor] :as dbg-state} original-form]
+  (let [condition (:break/when (meta form))]
+    (if condition
+      ;; If there is a condition and it is falsy, we need to skip
+      ;; the current level (:deeper than parent coor), but only
+      ;; once. Next time, we need to test the condition again.
+      `(let [old-breaks# @*skip-breaks*]
+         (when-not ~condition
+           (skip-breaks! :deeper ~(vec (butlast coor)) (:code (:msg ~'STATE__)) false))
+         (try
+           (expand-break ~form ~dbg-state ~original-form)
+           ;; in case :continue-all was requested in a deeper level
+           ;; we don't want go back to the old-breaks
+           (finally (when (not= :all (:mode @*skip-breaks*))
+                      (reset! *skip-breaks* old-breaks#)))))
+      `(expand-break ~form ~dbg-state ~original-form))))
+
 (defmacro breakpoint-if-interesting
   "Wrap form in a breakpoint if it looks interesting.
   Uninteresting forms are symbols that resolve to `clojure.core`
   (taking locals into account), and sexps whose head is present in
   `irrelevant-return-value-forms`. Used as :breakfunction in `tag-form`."
-  [form {:keys [coor] :as dbg-state} original-form]
+  [form dbg-state original-form]
   (if (uninteresting-form? &env form)
     form
-    (let [condition (:break/when (meta form))]
-      (if condition
-        ;; If there is a condition and it is falsy, we need to skip
-        ;; the current level (:deeper than parent coor), but only
-        ;; once. Next time, we need to test the condition again.
-        `(let [old-breaks# @*skip-breaks*]
-           (when-not ~condition
-             (skip-breaks! :deeper ~(vec (butlast coor)) (:code (:msg ~'STATE__)) false))
-           (try
-             (expand-break ~form ~dbg-state ~original-form)
-             ;; in case :continue-all was requested in a deeper level
-             ;; we don't want go back to the old-breaks
-             (finally (when (not= :all (:mode @*skip-breaks*))
-                        (reset! *skip-breaks* old-breaks#)))))
-        `(expand-break ~form ~dbg-state ~original-form)))))
+    `(breakpoint ~form ~dbg-state ~original-form)))
 
 (defmacro breakpoint-if-exception
   "Wrap form in a try-catch that has breakpoint on exception.
-  Ignores uninteresting forms.
-  Uninteresting forms are symbols that resolve to `clojure.core`
-  (taking locals into account), and sexps whose head is present in
-  `irrelevant-return-value-forms`. Used as :breakfunction in `tag-form`."
+  Used as :breakfunction in `tag-form`."
   [form dbg-state original-form]
-  (if (uninteresting-form? &env form)
-    form
-    `(try ~form
-          (catch Throwable ex#
-            (let [exn-message# (.getMessage ex#)
-                  break-result# (expand-break exn-message# ~dbg-state ~original-form)]
-              (if  (= exn-message# break-result#)
-                ;; if they continued then rethrow the exception
-                (throw ex#)
-                ;; otherwise return the value they injected
-                break-result#))))))
+  `(try ~form
+        (catch Throwable ex#
+          (let [~'STATE__ (assoc-in ~'STATE__ [:msg :caught-msg] (.getMessage ex#))
+                break-result#  (expand-break ex# ~dbg-state ~original-form)]
+            (if (= ex# break-result#)
+              ;; if they continued then rethrow the exception
+              (throw ex#)
+              ;; otherwise return the value they injected
+              break-result#)))))
 
 ;;; ## Data readers
 ;;
@@ -617,19 +627,19 @@ this map (identified by a key), and will `dissoc` it afterwards."}
 (defn breakpoint-reader
   "#break reader. Mark `form` for breakpointing."
   [form]
-  (ins/tag-form form #'breakpoint-with-initial-debug-bindings))
+  (ins/tag-form form #'breakpoint-with-initial-debug-bindings true))
 
 (defn debug-reader
   "#dbg reader. Mark all forms in `form` for breakpointing.
   `form` itself is also marked."
   [form]
   (ins/tag-form (ins/tag-form-recursively form #'breakpoint-if-interesting)
-                #'breakpoint-with-initial-debug-bindings))
+                #'breakpoint-if-interesting-with-initial-debug-bindings))
 
 (defn break-on-exception-reader
   "#exn reader. Wrap `form` in try-catch and break only on exception"
   [form]
-  (ins/tag-form form #'breakpoint-if-exception-with-initial-debug-bindings))
+  (ins/tag-form form #'breakpoint-if-exception-with-initial-debug-bindings true))
 
 (defn debug-on-exception-reader
   "#dbgexn reader. Mark all forms in `form` for breakpointing on exception.
@@ -655,6 +665,10 @@ this map (identified by a key), and will `dissoc` it afterwards."}
                 (eval form1)))
           (throw e))))))
 
+(def ^:dynamic *debug-data-readers*
+  "Reader macros like #dbg which cause code to be instrumented when present."
+  '#{dbg exn dbgexn break light})
+
 ;;; ## Middleware
 (defn- maybe-debug
   "Return msg, prepared for debugging if code contains debugging macros."
@@ -666,8 +680,8 @@ this map (identified by a key), and will `dissoc` it afterwards."}
         fake-reader (fn [x] (reset! has-debug? true) x)]
     (binding [*ns* (find-ns (symbol (or ns "user")))
               *data-readers* (->> (repeat fake-reader)
-                                  (interleave '[dbg exn dbgexn break light])
-                                  (apply assoc *data-readers*))]
+                                  (zipmap *debug-data-readers*)
+                                  (merge *data-readers*))]
       (try
         ;; new-line in REPL always throws; skip for debug convenience
         (when (> (count code) 3)
diff --git a/src/cider/nrepl/middleware/util/instrument.clj b/src/cider/nrepl/middleware/util/instrument.clj
index d236e941..1fb28238 100644
--- a/src/cider/nrepl/middleware/util/instrument.clj
+++ b/src/cider/nrepl/middleware/util/instrument.clj
@@ -225,9 +225,11 @@
     ;; Other coll types are safe, so we go inside them and only
     ;; instrument what's interesting.
     ;; Do we also need to check for seq?
-    coll? (doall (instrument-coll form))
+    coll? (cond-> (doall (instrument-coll form))
+            (::do-break (meta form)) (with-break))
     ;; Other things are uninteresting, literals or unreadable objects.
-    form))
+    (cond-> form
+      (::do-break (meta form)) (with-break))))
 
 ;;;; ## Pre-instrumentation
 ;;;
@@ -294,9 +296,14 @@
   "Tag form to be instrumented with breakfunction.
   This sets the ::breakfunction metadata of form, which can then be
   used by `instrument-tagged-code`. See this function for the meaning
-  of breakfunction."
-  [form breakfunction]
-  (m/merge-meta form {::breakfunction breakfunction}))
+  of breakfunction.
+  When `do-break?` is true it tells the instrumenter to wrap the form
+  with a breakpoint regardless of other heuristics."
+  ([form breakfunction]
+   (tag-form form breakfunction false))
+  ([form breakfunction do-break?]
+   (m/merge-meta form {::breakfunction breakfunction}
+     (when do-break? {::do-break true}))))
 
 (defn tag-form-recursively
   "Like `tag-form` but also tag all forms inside the given form."
diff --git a/src/data_readers.clj b/src/data_readers.clj
index c865dc09..713b31f8 100644
--- a/src/data_readers.clj
+++ b/src/data_readers.clj
@@ -1,5 +1,5 @@
 {dbg cider.nrepl.middleware.debug/debug-reader
  break cider.nrepl.middleware.debug/breakpoint-reader
- exn cider.nrepl.middleware.debug/break-on-exception-reader
- dbgexn cider.nrepl.middleware.debug/debug-on-exception-reader
+ break! cider.nrepl.middleware.debug/break-on-exception-reader
+ dbg! cider.nrepl.middleware.debug/debug-on-exception-reader
  light cider.nrepl.middleware.enlighten/light-reader}
diff --git a/test/clj/cider/nrepl/middleware/content_type_test.clj b/test/clj/cider/nrepl/middleware/content_type_test.clj
index 7c2d02aa..96677f3c 100644
--- a/test/clj/cider/nrepl/middleware/content_type_test.clj
+++ b/test/clj/cider/nrepl/middleware/content_type_test.clj
@@ -44,14 +44,15 @@
                         [:body :content-type :content-transfer-encoding :status]))))
 
   (testing "java.io.File"
-    (let [f (java.io.File/createTempFile "foo" ".txt")]
+    (let [f (java.io.File/createTempFile "foo" ".txt")
+          path (.getCanonicalPath f)]
       (is (= {:body ""
               :content-type
               ["message/external-body"
-               {:URL (str "file:" f) :access-type "URL"}]
+               {:URL (str "file:" path) :access-type "URL"}]
               :status #{"done"}}
              (-> {:op "eval"
-                  :code (str "(java.io.File. " (pr-str (str f)) ")")
+                  :code (str "(java.io.File. " (pr-str path) ")")
                   :content-type "true"}
                  session/message
                  (select-keys [:body :content-type :content-transfer-encoding :status]))))))
diff --git a/test/clj/cider/nrepl/middleware/debug_integration_test.clj b/test/clj/cider/nrepl/middleware/debug_integration_test.clj
index e5b574a8..f4ee21fa 100644
--- a/test/clj/cider/nrepl/middleware/debug_integration_test.clj
+++ b/test/clj/cider/nrepl/middleware/debug_integration_test.clj
@@ -136,18 +136,59 @@
         (f)))))
 
 (deftest debug-expression-test
+  (--> :eval "(ns user.test.debug)")
+  (<-- {:ns "user.test.debug"})
+  (<-- {:status ["done"]})
+
   (testing "normal eval (no debugging)"
     (--> :eval "(+ 2 3)")
     (<-- {:value "5"})
     (<-- {:status ["done"]}))
 
-  (testing "#break reader, no breakpoints"
-    ;; This code has only literals and core functions, so it should
-    ;; not break, but should just return the value
+  (testing "Top level breakpoints do not trigger"
+    ;; Since triggering a breakpoint to inspect the toplevel value is no different
+    ;; from returning the value.
     (--> :eval "#break (+ 2 3)")
     (<-- {:value "5"})
     (<-- {:status ["done"]}))
 
+  (testing "#break on return value of call"
+    (--> :eval "(do #break (+ 2 3) :ok)")
+    (<-- {:debug-value "5"})
+    (--> :next)
+    (<-- {:value ":ok"})
+    (<-- {:status ["done"]}))
+
+  (testing "#dbg reader on uninteresting forms"
+    ;; The return value of a def form is uninteresting, as well as the sub forms
+    ;; which are just core vars and literals.
+    ;; #dbg should not break on any of them and just return the value.
+    (--> :eval "(do #dbg (def foo [2 + {:skip \"me\"}]) foo)")
+    (<-- {:value "[2 #function[clojure.core/+] {:skip \"me\"}]"})
+    (<-- {:status ["done"]}))
+
+  (testing "#break reader on uninteresting forms"
+    ;; #break signifies that the user explicitly wants to set a breakpoint there,
+    ;; so ignore any of #dbg reader's heuristics on what an interesting form is.
+    (--> :eval "(let [a 1] #break [2 3], #break {:a a}, #break + #break (def foo 4) foo) ")
+    (<-- {:debug-value "[2 3]"})
+    (--> :next)
+    (<-- {:debug-value "{:a 1}"})
+    (--> :next)
+    (<-- {:debug-value "#function[clojure.core/+]"})
+    (--> :next)
+    (<-- {:debug-value "#'user.test.debug/foo"})
+    (--> :next)
+    (<-- {:value "4"})
+    (<-- {:status ["done"]}))
+
+  (testing "#break reader on literals"
+    ;; The instrumenter cannot tag literals which don't support metadata,
+    ;; so #break does not work on them.
+    (--> :eval "(do #break :kwd, #break 123, #break \"string\" :ok)")
+    (<-- {:value ":ok"})
+    (<-- {:status ["done"]}))
+
   (testing "#dbg reader, with breaks"
     (--> :eval
          "#dbg
diff --git a/test/clj/cider/nrepl/middleware/info_test.clj b/test/clj/cider/nrepl/middleware/info_test.clj
index d02c2504..62ca98a8 100644
--- a/test/clj/cider/nrepl/middleware/info_test.clj
+++ b/test/clj/cider/nrepl/middleware/info_test.clj
@@ -310,8 +310,8 @@
 
     (testing "java interop method with multiple classes"
       (let [response (session/message {:op "eldoc" :sym ".length" :ns "cider.nrepl.middleware.info-test"})]
-        (is (= (:class response)
-               ["java.lang.String" "java.lang.StringBuffer" "java.lang.CharSequence" "java.lang.StringBuilder"])
+        (is (every? (set (:class response)) ;; is a superset of:
+                    ["java.lang.String" "java.lang.StringBuffer" "java.lang.CharSequence" "java.lang.StringBuilder"])
             (pr-str response))
         (is (= (:member response) "length"))
         (is (not (contains? response :ns)))