nrepl

hlship 2025-06-30T22:00:12.368479Z

Hello! I'm having some trouble writing some middleware for nREPL. I'm trying to write a proper bit of middleware to enable org.clj-commons/pretty to override the default output of caught exceptions. What I'm finding is that (set! *caught-fn* repl/pretty-repl-caught) isn't working.

(ns clj-commons.pretty.nrepl
  "Middleware to setup pretty exception reporting in nREPL."
  {:added "3.5.0"}
  (:require [clj-commons.pretty.repl :as repl]
            [nrepl.middleware :as middleware]
            [nrepl.middleware.caught :as caught]))

(defn wrap-pretty
  "Ensures that exceptions are reported using pretty-repl-caught."
  [handler]
  (repl/install-pretty-exceptions)
  (prn :invoked `wrap-pretty)
  ; (alter-var-root #'caught/*caught-fn* (constantly repl/pretty-repl-caught))
  (fn with-pretty [msg]
    ;; Outer loop binds *caught-fn* to its default (clojure.main/repl-caught) for each request.
    ;; This update that per-thread binding before continuing to real handler.
    ;; This ensures it has the correct value when control returns to nrepl.middleware.caught/wrap-caught.
    (when (thread-bound? #'caught/*caught-fn*)
      (set! caught/*caught-fn* repl/pretty-repl-caught))
    (handler msg)))

(middleware/set-descriptor! #'wrap-pretty
                            {:doc      (-> #'wrap-pretty meta :doc)
                             :handles  {}
                             :requires #{#'caught/wrap-caught}
                             :expects  #{"eval"}})
What I'm seeing with this is that the default binding for *caught-fn*, clojure.main/repl-caught, refuses to be displaced. I had to put that wrapper around the call to set! because the code is generally receiving the the root binding for *caught-fn*.
$ DEBUG=T lein repl
Leiningen's classpath: /opt/homebrew/Cellar/leiningen/2.11.2/libexec/leiningen-2.11.2-standalone.jar
Applying task repl to []
Applying task javac to nil
Running javac with [@/var/folders/yg/vytvxpw500520vzjlc899dlm0000gn/T/.leiningen-cmdline11883427421637590331.tmp]
Applying task compile to nil
All namespaces already AOT compiled.
:invoked clj-commons.pretty.nrepl/wrap-pretty
nREPL server started on port 51183 on host 127.0.0.1 - 
:thread-bound? false
:thread-bound? false
REPL-y 0.5.1, nREPL 1.0.0
Clojure 1.12.1
OpenJDK 64-Bit Server VM 24.0.1+9-30
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

:thread-bound? false
user=> (first 1)
:thread-bound? false
Execution error (IllegalArgumentException) at user/eval3017 (form-init10014712483504165817.clj:1).
Don't know how to create ISeq from: java.lang.Long

user=>
So that's clojure.main/repl-caught doing the output, not pretty. I've tried swapping :requires and :exports in the middleware descriptor incase I have the logic backwards, but I think I have the order correct:
user=>   (some-> nrepl.middleware.dynamic-loader/*state* deref :stack)
(#'nrepl.middleware/wrap-describe #'nrepl.middleware.interruptible-eval/interruptible-eval #'nrepl.middleware.load-file/wrap-load-file #'clj-commons.pretty.nrepl/wrap-pretty #'nrepl.middleware.caught/wrap-caught #'nrepl.middleware.print/wrap-print #'nrepl.middleware.sideloader/wrap-sideloader #'nrepl.middleware.dynamic-loader/wrap-dynamic-loader #'nrepl.middleware.session/add-stdin #'nrepl.middleware.lookup/wrap-lookup #'nrepl.middleware.completion/wrap-completion #'nrepl.middleware.session/session)

hlship 2025-06-30T22:00:50.187979Z

rubber-duck I have got it working correctly, but I think there's some bugs in nREPL. Details shortly.

dpsutton 2025-06-30T22:09:42.737279Z

(hazy memory but each invocation of nrepl is a new clojure.main/repl and the bindings don’t survive. i think there are tickets about printing namespace maps, metadata and print widths)

dpsutton 2025-06-30T22:10:56.287369Z

https://github.com/nrepl/nrepl/issues/33

hlship 2025-06-30T22:11:45.687569Z

From what I can tell, attempting to bind *caught-fn* using set!` is hopeless, as: • the bound value is not ever used outside of creating a default binding used ultimately by nrepl.middleware.session • in theory, should be able to (set! *caught-fn* ...) to create the value that is passed to caught-transport, but if the caught key is nil, the caught-fn is removed (is this a bug?) Ah, all this merging is getting confusing, but we mege back in bound-configuraton (which is a function for some reason) and get caught-fn back BUT see all the notes about bindings. • So what works is to bind the caught to a symbol.

hlship 2025-06-30T22:11:49.290459Z

Or https://github.com/nrepl/nrepl/issues/334

hlship 2025-06-30T22:13:31.252839Z

(ns clj-commons.pretty.nrepl
  "Middleware to setup pretty exception reporting in nREPL."
  {:added "3.5.0"}
  (:require [clj-commons.pretty.repl :as repl]
            [nrepl.middleware :as middleware]
            [nrepl.middleware.caught :as caught]))

(defn wrap-pretty
  [handler]
  (repl/install-pretty-exceptions)
  (fn with-pretty
    [msg]
    (handler (assoc msg ::caught/caught `repl/pretty-repl-caught))))

(middleware/set-descriptor! #'wrap-pretty
                            {:doc      (-> #'wrap-pretty meta :doc)
                             :handles  {}
                             :requires #{}
                             :expects  #{"eval" #'caught/wrap-caught}})
So run before wrap-caught to tell it what symbol to resolve to find the right caught-fn. Perhaps could accomplish same with a binding. Let me try that next.

hlship 2025-06-30T22:16:35.607989Z

Nope, binding doesn't work. I'd like to figure out why not.

hlship 2025-06-30T22:25:01.559029Z

Not figuring it out ... but at least I have something that works.