Let's start with a simple one:
;; A simple function that always throw an exception
(defn my-throw-f [x]
(throw (ex-info (str "Invalid x: " x)
{:x x})))
=> #'user/my-throw-f
;; Let's call it
(my-throw-f "hello")
Execution error (ExceptionInfo) at user/my-throw-f (REPL:2).
Invalid x: hello
What I can understand from this error? Let's split it into blocks:
-
Execution error
: Say which Clojure
phase it happened. We will
learn more about it later.
-
ExceptionInfo
: The name of the class of the exception. ex-info
internally
creates an instance of clojure.lang.ExceptionInfo.
-
user/my-throw-f
: The name of the function where the exception was thrown
-
REPL:2
: The file name and the line of the file where this exception comes from. In that
case, we on REPL, so there is no file, but you can the that (throw ...
is written on the
second line and it's reported.
-
Invalid x: hello
: The message that you passed to ex-info
Now let's try an exception that was not created by us
;; Calling my-throw-f, that need 1 argument, with zero args.
(my-throw-f)
Execution error (ArityException) at user/eval137 (REPL:1).
Wrong number of args (0) passed to: user/my-throw-f
Let's see the blocks:
-
Execution error
: Nothing changed here
-
ArityException
: The name of the class of the exception. In that case, the full name is
java.lang.ArithmeticException
-
user/eval137
: Here a wired one. Once we are at Clojure repl, there is no function around
our exception. But here we can see an implementation detail: for every form evaluated in the REPL,
Clojure internally creates a function, generating a name probably with (gensym "eval")
,
then immediately executes it.
-
REPL:1
: Nothing new in here.
-
Wrong number of args (0) passed to: user/my-throw-f
: The message from the
exception
.
Now let's do a trick: create an anonymous function with a name and immediately invoke it.
((fn my-context []
(my-throw-f)))
Execution error (ArityException) at user/eval139$my-context (REPL:2).
Wrong number of args (0) passed to: user/my-throw-f
The context goes from user/eval137
, that means basically nothing to
user/eval139$my-context
that IMHO is way easier to find.
Let's use it to read our first stacktrace!
*e
#error {
:cause "Wrong number of args (0) passed to: user/my-throw-f"
:via
[{:type clojure.lang.ArityException
:message "Wrong number of args (0) passed to: user/my-throw-f"
:at [clojure.lang.AFn throwArity "AFn.java" 429]}]
:trace
[[clojure.lang.AFn throwArity "AFn.java" 429]
[clojure.lang.AFn invoke "AFn.java" 28]
[user$eval139$my_context__140 invoke "NO_SOURCE_FILE" 2]
...]}
Once we created a context called my-context
to our execution, anything before this context should
not be relevant.
Now let's try a harder one: an lazy stacktrace.
(map my-throw-f [1])
Error printing return value (ExceptionInfo) at user/my-throw-f (NO_SOURCE_FILE:2).
Invalid x: 1
Here a new thing: Error printing return value
. it's not a Execution error
anymore.
The code was executed, and returns a value. clojure.core/map
returns a lazy-seq, and when the
printer tryies to print the result, it throws.
Let's see the stacktrace
*e
#error {
:cause "Invalid x: 1"
:data {:x 1}
:via
[{:type clojure.lang.ExceptionInfo
:message nil
:data #:clojure.error{:phase :print-eval-result}
:at [clojure.main$repl$read_eval_print__9112 invoke "main.clj" 442]}
{:type clojure.lang.ExceptionInfo
:message "Invalid x: 1"
:data {:x 1}
:at [user$my_throw_f invokeStatic "NO_SOURCE_FILE" 2]}]
:trace
[[user$my_throw_f invokeStatic "NO_SOURCE_FILE" 2]
[user$my_throw_f invoke "NO_SOURCE_FILE" 1] ;; [6]
[clojure.core$map$fn__5885 invoke "core.clj" 2757] ;; [5]
[clojure.lang.LazySeq sval "LazySeq.java" 42]
[clojure.lang.LazySeq seq "LazySeq.java" 51] ;; [4]
[clojure.lang.RT seq "RT.java" 535]
[clojure.core$seq__5420 invokeStatic "core.clj" 139]
[clojure.core$print_sequential invokeStatic "core_print.clj" 53]
[clojure.core$fn__7331 invokeStatic "core_print.clj" 174]
[clojure.core$fn__7331 invoke "core_print.clj" 174]
[clojure.lang.MultiFn invoke "MultiFn.java" 234] ;; [3]
[clojure.core$pr_on invokeStatic "core.clj" 3662]
[clojure.core$pr invokeStatic "core.clj" 3665]
[clojure.core$pr invoke "core.clj" 3665]
[clojure.lang.AFn applyToHelper "AFn.java" 154]
[clojure.lang.RestFn applyTo "RestFn.java" 132]
[clojure.core$apply invokeStatic "core.clj" 667]
[clojure.core$prn invokeStatic "core.clj" 3702]
[clojure.core$prn doInvoke "core.clj" 3702] ;; [2]
[clojure.lang.RestFn invoke "RestFn.java" 408]
[clojure.main$repl$read_eval_print__9112 invoke "main.clj" 442] ;; [1]
[clojure.main$repl$fn__9121 invoke "main.clj" 458]
[clojure.main$repl invokeStatic "main.clj" 458]
[clojure.main$repl_opt invokeStatic "main.clj" 522]
[clojure.main$main invokeStatic "main.clj" 667]
[clojure.main$main doInvoke "main.clj" 616]
[clojure.lang.RestFn invoke "RestFn.java" 397]
[clojure.lang.AFn applyToHelper "AFn.java" 152]
[clojure.lang.RestFn applyTo "RestFn.java" 132]
[clojure.lang.Var applyTo "Var.java" 705]
[clojure.main main "main.java" 40]]}
I highlight some points of this stacktrace:
- 1: Where the
clojure.main
REPL start the
print
parse
- 2: The
clojure.main
uses the clojure.core/prn
. No magic in here.
- 3:
clojure.core/prn
uses clojure.core/print-method
. multimethod are ugly at
stacktraces. Is good to know that.
- 4: Here we are not printing anymore. The print methods invoked the method to evaluare the lazy seq.
- 5: We can see that this lazy was created inside
clojure.core/map
- 6: The lazy seq if finally calling
my-throw-f
Extra tips:
- The process that converts `my-throw-f` into `my_throw_f` is called munge and you can play with it using
clojure.core/munge
- You can use
clojure.repl/pst
to get a nice looking stacktrace print