dev-tooling

cfleming 2024-07-25T09:03:59.618199Z

While there's a lot of activity going on in nREPL, one thing that has always bugged me, and is more than a little inconvenient, is that it doesn't return any useful information on an eval exception - basically just the class of the exception. Is there any interest in fixing this? I see tonsky has a middleware in Clojure Sublimed, what are other editors doing to handle this?

oyakushev 2024-07-25T09:14:03.686439Z

Here's how an exception response looks like:

(<--
  id           "8"
  session      "1eb1c73c-1119-4f7b-9ca7-e8af1f1c5e8a"
  time-stamp   "2024-07-25 12:12:28.952840000"
  class        "java.lang.ArithmeticException"
  compile-like "false"
  message      "Divide by zero"
  phase        nil
  stacktrace   ((dict "class" "clojure.lang.Numbers" "file" "Numbers.java" "file-url" nil "flags"
       ("java")
       "line" 190 "method" "divide" "name" "clojure.lang.Numbers/divide" "type" "java")
 (dict "class" "clojure.lang.Numbers" "file" "Numbers.java" "file-url" nil "flags"
       ("dup" "java")
       "line" 3911 "method" "divide" "name" "clojure.lang.Numbers/divide" "type" "java")
 (dict "class" "user$eval7857" "file" "NO_SOURCE_FILE" "file-url" "" "flags"
       ("repl" "clj")
       "fn" "eval7857" "line" 43 "method" "invokeStatic" "name" "user$eval7857/invokeStatic" "ns" "user" "type" "clj" "var" "user/eval7857")
 (dict "class" "user$eval7857" "file" "NO_SOURCE_FILE" "file-url" "" "flags"
       ("dup" "repl" "clj")
       "fn" "eval7857" "line" 43 "method" "invoke" "name" "user$eval7857/invoke" "ns" "user" "type" "clj" "var" "user/eval7857")
 (dict "class" "clojure.lang.Compiler" "file" "Compiler.java" "file-url" nil "flags"
       ("tooling" "java")
       "line" 7194 "method" "eval" "name" "clojure.lang.Compiler/eval" "type" "java")
 (dict "class" "nrepl.middleware.interruptible_eval$evaluator$run__1505$fn__1516" "file" "interruptible_eval.clj" "file-url" "jar:file:/Users/alex/.m2/repository/nrepl/nrepl/99.99/nrepl-99.99.jar!/nrepl/middleware/interruptible_eval.clj" "flags"
       ("tooling" "clj")
       "fn" "evaluator/run/fn" "line" 106 "method" "invoke" "name" "nrepl.middleware.interruptible_eval$evaluator$run__1505$fn__1516/invoke" "ns" "nrepl.middleware.interruptible-eval" "type" "clj" "var" "nrepl.middleware.interruptible-eval/evaluator")
 (dict "class" "nrepl.middleware.interruptible_eval$evaluator$run__1505" "file" "interruptible_eval.clj" "file-url" "jar:file:/Users/alex/.m2/repository/nrepl/nrepl/99.99/nrepl-99.99.jar!/nrepl/middleware/interruptible_eval.clj" "flags"
       ("tooling" "clj")
       "fn" "evaluator/run" "line" 101 "method" "invoke" "name" "nrepl.middleware.interruptible_eval$evaluator$run__1505/invoke" "ns" "nrepl.middleware.interruptible-eval" "type" "clj" "var" "nrepl.middleware.interruptible-eval/evaluator")
 (dict "class" "nrepl.middleware.session$session_exec$session_loop__1584" "file" "session.clj" "file-url" "jar:file:/Users/alex/.m2/repository/nrepl/nrepl/99.99/nrepl-99.99.jar!/nrepl/middleware/session.clj" "flags"
       ("tooling" "clj")
       "fn" "session-exec/session-loop" "line" 230 "method" "invoke" "name" "nrepl.middleware.session$session_exec$session_loop__1584/invoke" "ns" "nrepl.middleware.session" "type" "clj" "var" "nrepl.middleware.session/session-exec")
 (dict "class" "nrepl.SessionThread" "file" "SessionThread.java" "file-url" nil "flags"
       ("tooling" "java")
       "line" 21 "method" "run" "name" "nrepl.SessionThread/run" "type" "java"))
)
What extra information would you prefer to see?

cfleming 2024-07-25T09:22:23.729979Z

I don't see that, I only see:

{:ex "class java.lang.RuntimeException", :id "5a032217-15c6-4fa1-ab9f-08602346ebf6", :root-ex "class java.lang.RuntimeException", :session "94fdbd71-384e-4a4f-b96c-5139e97422bd", :status ["eval-error"]}

cfleming 2024-07-25T09:22:37.477099Z

Is that coming from a middleware?

cfleming 2024-07-25T09:23:50.466589Z

The doc also states that only :ex and :root-ex come back: https://nrepl.org/nrepl/ops.html#eval

oyakushev 2024-07-25T09:59:36.297849Z

Ah, I'm sorry, I ran that with cider-nrepl enabled, let me try without it.

oyakushev 2024-07-25T10:03:33.085919Z

Right, I get what you get now. Apparently, it's cider-nrepl middleware that adds extra fields.

oyakushev 2024-07-25T10:05:19.784669Z

I guess you can look at the state of *e after the evaluation or you can pass a :nrepl.middleware.caught/caught-fn callback in the nREPL message to have the exception delivered to a custom callback (that should exist on the server, though).

oyakushev 2024-07-25T10:07:15.877379Z

I don't know why only the exception class is returned in the base response, maybe @bozhidar can fill in.

bozhidar 2024-07-25T12:03:56.423719Z

@alexyakushev Just legacy. This is how the eval middleware was behaving originally and I never bothered to changed it, as the stacktrace middleware in cider-nrepl provided the richer exceptions that I was after.

👍 2
bozhidar 2024-07-25T12:04:24.755319Z

Probably it won't hurt to enhance the built-in responses at some point in some backward compatible manner.

cfleming 2024-07-25T20:42:51.675349Z

Right, it seems like as long as the change is only additive, it shouldn't break clients.

cfleming 2024-07-25T20:44:25.742999Z

I do have some custom code I load into REPLs, so maybe the caught-fn callback is the easiest solution, but it seems like it would be good to have the out of the box experience be nicer.

oyakushev 2024-07-25T20:56:40.392569Z

Adding fields wouldn't probably be breaking, but I'd be careful about adding too much. Error message? Probably fine, those are usually short. But I don't know about including a stacktrace for example – it raises the question how does nREPL encodes the stacktrace, does it send each frame as data (like cider-nrepl does) or formats them as strings, does it preserve all the causal parents, etc. Given that *e and :caught-fn is still available, it makes more sense to access the information needed to the client (and in a format convenient for the client) through those.

bozhidar 2024-07-26T05:04:40.288869Z

Yeah, I definitely think we shouldn't include the stacktrace, as it adds a lot of complexity.

bozhidar 2024-07-26T05:07:39.684249Z

@cfleming Can you share how an ideal error response should look like for your needs?

cfleming 2024-07-30T18:23:47.296009Z

I'd definitely include both the error message and the stacktrace, since any client is going to want to show both. I can work around it using :caught-fn, but the default response at the moment is basically totally useless, which doesn't seem like a great experience.

cfleming 2024-07-30T18:39:45.207189Z

One thing that I'm not clear on, it looks like I can't use caught-fn to return the exception data? All the examples in the doc are used for printing, but it's not clear from the doc whether the value that the fn returns comes back in the response. Looking at the code, it looks like not. I think ideally I'd just return the #error object in the response, or something morally equivalent in older Clojure versions, but it's not clear to me how to achieve that without having to do a second round trip for *e.

oyakushev 2024-07-30T18:43:00.627779Z

No, it does not substitute the to-be response value with the result of :caught-fn call. The callback is called separately and has to do a side effect

cfleming 2024-07-30T18:44:21.682859Z

So is it fair to say that with vanilla nREPL there's no way to return more information about the error without round-tripping?

oyakushev 2024-07-30T18:45:47.748679Z

Yes, it is like that. Middleware is the preferred way to customize such behavior.

cfleming 2024-07-30T18:47:53.032049Z

Boo. I definitely think this is a sub-par out of the box experience. I understand that there's complexity around e.g. ClojureScript, but just getting back the exception class is useless. I'm not really proposing a change here because I have to support older nREPL versions, I'm just grumbling 🙂