Note: Still a WIP
Problems
- Errors can occur in several phases but all get reported in the same way
- Can we distinguish between these phases based on where it occurs?
- How can we format exceptions for each kind of exception
Execution Phases
Job | Phase | What is thrown intentionally? | What is thrown accidentally? | Where thrown? | Detect where? | Message template | Example error | Example message | Message in 1.9 |
---|---|---|---|---|---|---|---|---|---|
REPL | Read / reading source | IOEx, RuntimeEx, NumberFormatEx, IllegalArgumentEx, IllegalStateEx, UnsupportedOpEx | N/A | LispReader wrapped in ReaderException in LispReader.read() | Catch during read in clojure.main/repl-read | Syntax error reading at (LINE:COL). Cause: CAUSE | :::5 | Syntax error reading at (1:1). Cause: Invalid token: :::5 | RuntimeException Invalid token: :::5 clojure.lang.Util.runtimeException (Util.java:221) |
compile | Read / reading source | IOEx, RuntimeEx, NumberFormatEx, IllegalArgumentEx, IllegalStateEx, UnsupportedOpEx | N/A | LispReader wrapped in ReaderException in LispReader.read() ReaderException replaced with CompilerException in Compiler.compile() | Catch from LispReader.read() in Compiler.compile() | Syntax error reading source file at (SOURCE:LINE:COL). Cause: CAUSE | :::5 in .clj file | Syntax error reading source file at (foo.clj:2:0). Cause: Invalid token: :::5 | RuntimeException Invalid token: :::5 clojure.lang.Util.runtimeException (Util.java:221) |
REPL | compile | compile / macroexpand (spec) | ExceptionInfo w/spec keys | N/A | Compiler.checkSpecs() | Catch from checkSpecs in Compiler.macroexpand1() | Syntax error macroexpanding MACRONAME at (SOURCE:LINE:COL): | (let [x]) | Syntax error macroexpanding clojure.core/let at (foo.clj:10:5): | CompilerException clojure.lang.ExceptionInfo: Call to clojure.core/let did not conform to spec: |
compile | compile / macroexpand (thrown by macro) | thrown by core defmacro impls: IllegalArgEx, IllegalStateEx, Ex, ExInfo community is same | N/A | in macro implementations under Compiler.macroexpand() | Catch during invocation of macro in Compiler.macroexpand1() | Syntax error macroexpanding MACRONAME at (SOURCE:LINE:COL). Cause: CAUSE | (cond 1) | Syntax error macroexpanding clojure.core/cond at (foo.clj:10:5). Cause: cond requires an even number of forms | IllegalArgumentException cond requires an even number of forms clojure.core/cond (core.clj:600) |
REPL | compile | compile / macroexpand (accidental throw in macro) | N/A | everything but intentional exs in prior row | in macro implementations under Compiler.macroexpand() | " | Unexpected error macroexpanding MACRONAME at (SOURCE:LINE:COL). Cause: EXCLASS CAUSE | (defmulti 5 class) | Unexpected error macroexpanding clojure.core/defmulti at (foo.clj:10:5). Cause: ClassCastException java.lang.Long cannot be cast to clojure.lang.IObj | ClassCastException java.lang.Long cannot be cast to clojure.lang.IObj clojure.core/with-meta--5142 (core.clj:217) |
REPL | compile | compile | IOEx, RuntimeEx, NumberFormatEx, IllegalArgumentEx, IllegalStateEx, UnsupportedOpEx, ArityEx, ExceptionInfo, ReflectiveOperationEx | not worrying about for now | Compiler expr parsers wrapped in CompilerException in Compiler.compile() | Catch in analyze or eval in Compiler.compile1() | Syntax error compiling EXPRNAME at (SOURCE:LINE:COL). Cause: CAUSE | (def 5) | Syntax error compiling def at (foo.clj:10:5). Cause: First argument to def must be a Symbol | CompilerException java.lang.RuntimeException: First argument to def must be a Symbol, compiling:(foo.clj:5:1) |
REPL | runtime | evaluation | any (except AssertionError or spec ExceptionInfo) | don't differentiate | any function | Catch in call to eval in clojure.main/repl | Evaluation error at STACKFN (STACKSRC:LINE). EXCLASS CAUSE | (/ 1 0) | Evaluation error at clojure.lang.Numbers.divide (Numbers.java:163). ArithmeticException Divide by zero | ArithmeticException Divide by zero clojure.lang.Numbers.divide (Numbers.java:163) |
REPL | runtime | evaluation / spec instrument | ExceptionInfo w/spec keys | N/A | instrumented spec | " | Evaluation error at STACKFN (STACKSRC:LINE), did not conform to spec: | (f :a) | Evaluation error at user/f (foo.clj:10), did not conform to spec: | ExceptionInfo Call to #'user/f did not conform to spec: |
REPL | runtime | evaluation / assertion failure or pre/post | AssertionError | N/A | any function | " | Evaluation error at STACKFN (STACKSRC:LINE), CAUSE | (assert false) | Evaluation error at user/eval149 (NO_SOURCE_FILE:8), Assert failed: false | AssertionError Assert failed: false user/eval149 (NO_SOURCE_FILE:8) |
REPL | N/A | all | any function | Catch in call to print in clojure.main/repl | Error printing return value at STACKFN (STACKSRC:LINE), encountered: EXCLASS CAUSE | ? | Error printing return value at some/thing (foo.clj:100). ArithmeticException Divide by 0 |
Background info
CompilerException
- conveys (only) location, but nothing else (no message)
- if nested (not sure if this is ever a thing), CompilerExceptions do not add value, only want the bottom one as it is most likely to have the correct location
- CompilerException has fields for source %2B line but not does store col in a field, just dumps it into the message
- it's useful for the default message of a CompilerException to include it's cause message and location info in case this is caught by a bit of tooling
- this is the message being checked in most of the "compile error message" tests in Clojure
- but the repl can ignore that message completely and build something better
General exception chain form:
- wrapper chain (usually 0 or 1)
- always CompilerException, conveys Clojure source location (source/line/col)
- ignore all but bottom one
- no message - but message could be dynamic and convey cause info
- cause chain
- possibly nested, if nested ignore all but original one (most nested), let's call that the init cause
- init cause typically has most useful message (something like an NPE may not)
- init cause stack trace is also the most useful usually - conveys Java and/or Clojure source/line info via stack trace elements
- init cause may be an ExceptionInfo conveying additional ex-data map
Attributes available when choosing how to display:
- If compile error, from wrapper:
- Clojure source file (can be special NO_SOURCE_PATH value)
- Clojure line
- Clojure column
- Init cause:
- Message
- Exception info map (if ExceptionInfo)
- Cause exception class (probably not useful if RuntimeException, Exception, etc). ever relevant?
- Other common ones are stuff in java.lang - IllegalArg, NPE, IllegalState, UnsupportedOp, Runtime, E
- Stack trace (top element conveys class name/line)
- Stack trace element has class name, method name, file name, and line number)
4 Comments
Hide/Show CommentsJul 16, 2018
Bruce Hauman
It might be helpful to add an additional category.
Clojure Core Macroexpansion: These core macro exceptions are extremely common, and while they are technically macroexpansion errors, the coding situation/use-case is more similar to a syntax error. In this case, the stacktrace has much less value than the location in user code that triggered it.
The desired "Printed error" behavior is similar to the desired behavior for Macroexpansion spec errors, I really want to see the location in my code where I caused this to occur.
Jul 16, 2018
Alex Miller
I don't think this is a special case - the same would be of true of your code calling a macro in a library (or in your own code). I've been experimenting with capturing the macro form and printing in this case and it's pretty useful (but you can only go one "level", as macroexpansion is happening in a loop). Even so, it seems quite helpful.
Aug 11, 2018
Erik Assum
Any chance of "wrong arity" being considered into this?
"clojure.lang.ArityException: Wrong number of args (2) passed to: foo"
I really miss
"clojure.lang.ArityException: Wrong number of args (2) passed to: foo, expected [3]"
I appreciate that this may be hard with multiarity fns, but still.
Aug 11, 2018
Alex Miller
This page is really about how to categorically throw and print classes of errors, not about any particular error (there are some examples here, but just to serve as examples of a class). So the best way to approach this is to file a jira.
Functions of course have multiple and possibly variable arity and function objects have no notion of “expected” arity. So I’d say it’s unlikely this is something we would do as described. But stepping back, the problem is - you invoked it wrong, can we give you more feedback to invoke it correctly, and that’s maybe a better problem (this starts to include spec instrument errors too for example).