Clojure

Improve error printing in clojure.main with -m, -e, etc

Details

  • Type: Enhancement Enhancement
  • Status: Open Open
  • Priority: Critical Critical
  • Resolution: Unresolved
  • Affects Version/s: Release 1.10
  • Fix Version/s: Release 1.11
  • Component/s: None
  • Approval:
    Vetted

Description

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):

$ clj -e "(ns foo (import java.util.Date))"
Exception in thread "main" Syntax error macroexpanding clojure.core/ns at (1:1).
Call to clojure.core/ns did not conform to spec.
	at clojure.lang.Compiler.checkSpecs(Compiler.java:6971)
	at clojure.lang.Compiler.macroexpand1(Compiler.java:6987)
	at clojure.lang.Compiler.macroexpand(Compiler.java:7074)
	at clojure.lang.Compiler.eval(Compiler.java:7160)
	at clojure.lang.Compiler.eval(Compiler.java:7131)
	at clojure.core$eval.invokeStatic(core.clj:3214)
	at clojure.main$eval_opt.invokeStatic(main.clj:465)
	at clojure.main$eval_opt.invoke(main.clj:459)
	at clojure.main$initialize.invokeStatic(main.clj:485)
	at clojure.main$null_opt.invokeStatic(main.clj:519)
	at clojure.main$null_opt.invoke(main.clj:516)
	at clojure.main$main.invokeStatic(main.clj:598)
	at clojure.main$main.doInvoke(main.clj:561)
	at clojure.lang.RestFn.applyTo(RestFn.java:137)
	at clojure.lang.Var.applyTo(Var.java:705)
	at clojure.main.main(main.java:37)
Caused by: clojure.lang.ExceptionInfo: Call to clojure.core/ns did not conform to spec. {:clojure.spec.alpha/problems [{:path [], :reason "Extra input", :pred (clojure.spec.alpha/cat :docstring (clojure.spec.alpha/? clojure.core/string?) :attr-map (clojure.spec.alpha/? clojure.core/map?) :ns-clauses :clojure.core.specs.alpha/ns-clauses), :val ((import java.util.Date)), :via [:clojure.core.specs.alpha/ns-form], :in [1]}], :clojure.spec.alpha/spec #object[clojure.spec.alpha$regex_spec_impl$reify__2509 0x412c995d "clojure.spec.alpha$regex_spec_impl$reify__2509@412c995d"], :clojure.spec.alpha/value (foo (import java.util.Date)), :clojure.spec.alpha/args (foo (import java.util.Date))}
	at clojure.spec.alpha$macroexpand_check.invokeStatic(alpha.clj:705)
	at clojure.spec.alpha$macroexpand_check.invoke(alpha.clj:697)
	at clojure.lang.AFn.applyToHelper(AFn.java:156)
	at clojure.lang.AFn.applyTo(AFn.java:144)
	at clojure.lang.Var.applyTo(Var.java:705)
	at clojure.lang.Compiler.checkSpecs(Compiler.java:6969)
	... 15 more

As a macroexpansion spec error, this could just say:

$ clj -e "(ns foo (import java.util.Date))"
Syntax error macroexpanding clojure.core/ns at (1:1).
((import java.util.Date)) - failed: Extra input spec: :clojure.core.specs.alpha/ns-form

which would be much less scary.

Activity

Hide
Alex Miller added a comment -

Attached a proof-of-concept hack at this. Needs more discussion about what to do about the stack traces, whether we can do a better job sharing code between clojure.main and clojure.repl/pst, and maybe should consider an option (like -x) to dump the full stack chain in case you need it for debugging.

Show
Alex Miller added a comment - Attached a proof-of-concept hack at this. Needs more discussion about what to do about the stack traces, whether we can do a better job sharing code between clojure.main and clojure.repl/pst, and maybe should consider an option (like -x) to dump the full stack chain in case you need it for debugging.
Hide
Stuart Halloway added a comment -

It is important to keep in mind all the ways that clojure.main is different from the REPL. When running the REPL, you still have the process after an exception happens. If you don't like the way exceptions are presented, you can programmatically grab the exception and do whatever your want. You can use the new 1.10 tools or write your own.

When clojure.main exits, your process is gone. If we make any change at all (particularly eliding detail for human reader convenience), and somebody wants something different, then they are out of luck. Also, printing the stack trace as Java does (the current behavior) means we are operationally compatible with production tooling designed around Java processes.

Most of the reported pain people feel from this would be fixed by making a tiny number of changes in how Java processes are launched from lein, boot, Cursive, et al. On the other side of such changes, these tools would have independent and full control of how users experience errors. They could match 1.10 behavior or enhance it, separate from the Clojure release cycle. And they could make different choices in different contexts.

Show
Stuart Halloway added a comment - It is important to keep in mind all the ways that clojure.main is different from the REPL. When running the REPL, you still have the process after an exception happens. If you don't like the way exceptions are presented, you can programmatically grab the exception and do whatever your want. You can use the new 1.10 tools or write your own. When clojure.main exits, your process is gone. If we make any change at all (particularly eliding detail for human reader convenience), and somebody wants something different, then they are out of luck. Also, printing the stack trace as Java does (the current behavior) means we are operationally compatible with production tooling designed around Java processes. Most of the reported pain people feel from this would be fixed by making a tiny number of changes in how Java processes are launched from lein, boot, Cursive, et al. On the other side of such changes, these tools would have independent and full control of how users experience errors. They could match 1.10 behavior or enhance it, separate from the Clojure release cycle. And they could make different choices in different contexts.
Hide
Alex Miller added a comment -

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.

Show
Alex Miller added a comment - 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.

People

Vote (2)
Watch (6)

Dates

  • Created:
    Updated: