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?
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?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"]}Is that coming from a middleware?
The doc also states that only :ex and :root-ex come back: https://nrepl.org/nrepl/ops.html#eval
Ah, I'm sorry, I ran that with cider-nrepl enabled, let me try without it.
Right, I get what you get now. Apparently, it's cider-nrepl middleware that adds extra fields.
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).
I don't know why only the exception class is returned in the base response, maybe @bozhidar can fill in.
@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.
Probably it won't hurt to enhance the built-in responses at some point in some backward compatible manner.
Right, it seems like as long as the change is only additive, it shouldn't break clients.
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.
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.
Yeah, I definitely think we shouldn't include the stacktrace, as it adds a lot of complexity.
@cfleming Can you share how an ideal error response should look like for your needs?
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.
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.
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
So is it fair to say that with vanilla nREPL there's no way to return more information about the error without round-tripping?
Yes, it is like that. Middleware is the preferred way to customize such behavior.
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 🙂