Clojure 1.10 has a lot of improvements to print better errors in clojure.main when using the repl. However, these improvements do not extend to the default behavior when using other clojure.main modes like -m (run -main in a namespace), -e (run expression), or the script running option. All of these will simply let an exception throw out of the main and default to JVM behavior, which prints the stack of the full chain of exceptions.
Some practical ramifications of this are that anything in the ecosystem that is running Clojure programs via clojure.main (could be clj, but could also be other tools like "lein ring server" where I saw this happenn), these tools are not getting the advantages of the error improvements.
The general proposal is to catch Throwable around the call to the main entry point and at least run the Throwable->map, ex-triage, ex-str pipeline. Also need to consider what to do about printing the stack trace. The default JVM behavior is to print the stack of every exception in the chain, but printing either the root cause stack (in execution error) or no stack (in reader/compiler/macroexpansion cases) would likely be better.
Simple repro (note very long line of spec problem data that all gets spammed to the screen - have to scroll right to see it):
As a macroexpansion spec error, this could just say:
which would be much less scary.
Proposal: clojure.main is a process boundary. As such, the facilities for communicating information through it are limited to printing and the return value. Existing consumers of clojure.main are likely to be using the return value so that needs to be maintained. For the printing, we have expended a lot of energy to craft a good message for all phases in the 1.10 error message re-work, which is currently active at the repl. For other consumers of clojure.main, we wish to give them the same experience, but also want don't want to throw away the full exception chain / stack trace info for consumers that need it. In the repl, it's less critical to retain all info because further interactive calls can be made for pst or ex-data, but in a clojure.main exit, you're done.
As such, the attached patch saves all that information and puts it in a temp file (falling back to printing to stderr if the file can't be created) and prints just the error message you would get at the repl.
One additional change included in the patch is that the current ex-triage does not preserve and ex-str does not print the full path to the source file, just the file name itself. If the error is in, say core.clj, this is not helpful. So I've made an additive change there to also store the path and use it in the error message you see at both the REPL and in clojure.main.
For the example above, you'd see something like:
For an example with a namespace path:
The temp file created is a readable .edn file which can be further processed by tooling if needed:
Ideally, there should be both a programmatic entry point and a command line wrapper entry point that prints rather than leaves it to the jvm.
Added updated -3 patch
Because leiningen relies on forking commands as a clojure.main process in many cases, this will also affect leiningen output. Example (annotated left column notes who is printing):
The double "Syntax error compiling" line is because Leiningen is printing in addition to clojure.main. Leiningen could take more control over what happens here if desired. Relevant code: https://github.com/technomancy/leiningen/blob/ba9d2ac9920e6622c944fcf223debbeadf85222d/src/leiningen/compile.clj#L150-L171
-4 patch adds explicit clojure.main option --report-stderr to force reporting to stderr
-5 patch reworks the error reporting stuff away from a private macro and into a public function that can be reused with flags for reporting target and system/exit.