ClojureErrorReporting

ThoughtStorms Wiki

ClojureLanguage's error reporting / handling is notoriously bad.

This argues it's simply absent. https://lispcast.com/clojure-error-messages-accidental/

Which is kind of true and obvious when you think about it.

(See also : AgainstClojure)

Quora Answer : What is clojure bad at?

Mar 10, 2020

There are things that Clojure wasn't designed for, and isn't intended for.

To complain that Clojure is "bad" at type-checking is a bit like worrying that your car doesn't dance. Clojure is built on the philosophy that you should use other strategies to get the effects of reliability that static typing promises.

I wouldn't say that Clojure is "bad" at static types. It's just against them.

As to tail call optimization. Yes it doesn't do it. That's a trade-off. In order to work in the world of the JVM and be compatible with Java it uses Java's own functions and dispatch mechanism, which means because the JVM doesn't have TCO, Clojure doesn't either. It isn't implementing its own "Clojure-machine" on top of Java. A Lisp could do that too, and then it would have TCO, but it would be adding a whole other layer of VM.

However, the loop/recur mechanism gives you what you need from TCO. (A memory efficient way of writing recursion that doesn't consume extra memory for each "call" ).

And it's not like TCO is transparent in other FP languages. You still have to write functions in a specific, slightly unnatural and long-winded way, to get the virtue of them. Using loop / recur really isn't worse than that. And, in some ways, I think it's "better" in that it's explicit. You don't think you're getting the benefits of TCO and then because of some mistake in the way you wrote your code, not get it. To do loop/recur work at all, you must be putting your algorithm into an appropriate form.

But there's one thing that Clojure sure is bad at ... not just bad but abysmally awful. Just totally mind-blowingly fucking crap at.

(And note that I say this as a passionate lover of Clojure; it's my favouritest language ever.)

But there is one thing that it is truly dire at which it surely ought not to be.

And that is reporting errors.

Never has such a great language had such lousy error reporting.

This is a bug I'm working on right now :

ERROR in (fsquery) (Reflector.java:426)

Go

expected: (= false ((file-tests fsq4) #:fsnode{abs "abc.txt"}))

actual: java.lang.NullPointerException: null

at clojure.lang.Reflector.invokeNoArgInstanceMember (Reflector.java:426)

fsquery.fsnode$abs.invokeStatic (fsnode.clj:34)

fsquery.fsnode$abs.invoke (fsnode.clj:33)

fsquery.coretest$eval635$fn684$fn713.invoke (coretest.clj:147)

fsquery.core$file_tests$f533$fn534.invoke (core.clj:66)

clojure.core$map$fn__5851.invoke (core.clj:2755)

clojure.lang.LazySeq.sval (LazySeq.java:42)

clojure.lang.LazySeq.seq (LazySeq.java:51)

clojure.lang.RT.seq (RT.java:531)

clojure.core$seq__5387.invokeStatic (core.clj:137)

clojure.core$everyQMARK.invokeStatic (core.clj:2679)

clojure.core$everyQMARK.invoke (core.clj:2672)

fsquery.core$file_tests$f__533.invoke (core.clj:67)

fsquery.coretest$eval635$fn684$fn737.invoke (coretest.clj:166)

fsquery.coretest$eval635$fn__684.invoke (coretest.clj:166)

clojure.test$test_var$fn__9707.invoke (test.clj:717)

clojure.test$test_var.invokeStatic (test.clj:717)

clojure.test$test_var.invoke (test.clj:708)

clojure.test$test_vars$fn9733$fn9738.invoke (test.clj:735)

clojure.test$default_fixture.invokeStatic (test.clj:687)

clojure.test$default_fixture.invoke (test.clj:683)

clojure.test$test_vars$fn__9733.invoke (test.clj:735)

clojure.test$default_fixture.invokeStatic (test.clj:687)

clojure.test$default_fixture.invoke (test.clj:683)

clojure.test$test_vars.invokeStatic (test.clj:731)

clojure.test$testallvars.invokeStatic (test.clj:737)

clojure.test$test_ns.invokeStatic (test.clj:758)

clojure.test$test_ns.invoke (test.clj:743)

user$eval224$fn__285.invoke (form-init8498298680033924773.clj:1)

clojure.lang.AFn.applyToHelper (AFn.java:156)

clojure.lang.AFn.applyTo (AFn.java:144)

clojure.core$apply.invokeStatic (core.clj:667)

clojure.core$apply.invoke (core.clj:660)

leiningen.core.injected$compose_hooks$fn__154.doInvoke (form-init8498298680033924773.clj:1)

clojure.lang.RestFn.applyTo (RestFn.java:137)

clojure.core$apply.invokeStatic (core.clj:665)

clojure.core$apply.invoke (core.clj:660)

leiningen.core.injected$run_hooks.invokeStatic (form-init8498298680033924773.clj:1)

leiningen.core.injected$run_hooks.invoke (form-init8498298680033924773.clj:1)

leiningen.core.injected$prepareforhooks$fn159$fn160.doInvoke (form-init8498298680033924773.clj:1)

clojure.lang.RestFn.applyTo (RestFn.java:137)

clojure.lang.AFunction$1.doInvoke (AFunction.java:31)

clojure.lang.RestFn.invoke (RestFn.java:408)

clojure.core$map$fn__5851.invoke (core.clj:2755)

clojure.lang.LazySeq.sval (LazySeq.java:42)

clojure.lang.LazySeq.seq (LazySeq.java:51)

clojure.lang.Cons.next (Cons.java:39)

clojure.lang.RT.boundedLength (RT.java:1788)

clojure.lang.RestFn.applyTo (RestFn.java:130)

clojure.core$apply.invokeStatic (core.clj:667)

clojure.test$run_tests.invokeStatic (test.clj:768)

clojure.test$run_tests.doInvoke (test.clj:768)

clojure.lang.RestFn.applyTo (RestFn.java:137)

clojure.core$apply.invokeStatic (core.clj:665)

clojure.core$apply.invoke (core.clj:660)

user$eval224$fn297$fn330.invoke (form-init8498298680033924773.clj:1)

user$eval224$fn297$fn298.invoke (form-init8498298680033924773.clj:1)

user$eval224$fn__297.invoke (form-init8498298680033924773.clj:1)

user$eval224.invokeStatic (form-init8498298680033924773.clj:1)

user$eval224.invoke (form-init8498298680033924773.clj:1)

clojure.lang.Compiler.eval (Compiler.java:7176)

clojure.lang.Compiler.eval (Compiler.java:7166)

clojure.lang.Compiler.load (Compiler.java:7635)

clojure.lang.Compiler.loadFile (Compiler.java:7573)

clojure.main$load_script.invokeStatic (main.clj:452)

clojure.main$init_opt.invokeStatic (main.clj:454)

clojure.main$init_opt.invoke (main.clj:454)

clojure.main$initialize.invokeStatic (main.clj:485)

clojure.main$null_opt.invokeStatic (main.clj:519)

clojure.main$null_opt.invoke (main.clj:516)

clojure.main$main.invokeStatic (main.clj:598)

clojure.main$main.doInvoke (main.clj:561)

clojure.lang.RestFn.applyTo (RestFn.java:137)

clojure.lang.Var.applyTo (Var.java:705)

clojure.main.main (main.java:37)

This might well be a really simple error. Probably I just typed something wrong somewhere. Or maybe it got that null value by looking for the wrong key in a map. Or trying to take the head of an empty list or something.

But look.

Firstly where is this error?

Reflector.java:426

That's ... well .. not only is Reflector NOT a file in my project. And not a file which is written by me. It's not even a Clojure file. This is an exception which has fired in the Java code that is effectively the infrastructure that is running my Clojure program.

But the stack trace can't tell the difference.

I guess maybe that's the cost of a language that compiles to the same Java byte-code and has two-way interop with Java : the language can't distinguish the programmers' code compiled into Java, from the libraries and infrastructure in Java that it uses.

The next lines DO tell me which of my functions the error crops up in. The function abs inside my module fsnode.

However, I have no idea from any of this stack trace the most important piece of information I want answered : which of the variables in my code on line 34 of fsnode, actually has the null value that is causing this exception?

Why not? Why can't Clojure tell me this? Probably because the name of the variable got thrown away somewhere in the complex compilation.

Beyond that. I know which unit-test defined the call to function abs to trigger the error. (But I'd have known that anyway, thanks to the magic of test-driven development, it's kind of obvious what I'm working on.) So I will be able to use print debugging to figure out which variable is null.

But because that's an anonymous function, it's telling me where it's defined but not from where it was called. So I can only put the print debug in that anonymous function itself, so my output will now be cluttered by printing that value in all the other calls to that function that aren't involved in this failure.

And because the outputs of print debugging are a bit separate from the stack trace, I have screens and screens to go through in the terminal.

Meanwhile, from lines 10 to 79 of that stack trace are basically useless junk. They largely tell me about how the test-runner came to call the unit test. Again, this is infrastructure that isn't involved in my bug and isn't interesting to me.

And this is a common experience. I'm doing test-driven development. Which is the way I believe we should be working. But both the language and the test-framework actually get in the way of decent error reports.

The other thing which makes tracking errors hard in Clojure is that when you try to access a key in a map that isn't there. Or take the head of an empty list, the failure is silent. You just get a null back. This, I think, is a bad design decision in the language. There should at least be a compiler option to tell Clojure "throw an exception when trying to get a non-existent item". This is what Python, for example, would do by default.

At least that would tell you exactly where your problem was happening, rather than waiting until the returned null ends up causing trouble somewhere else.

I don't know if there's a reason for why Clojure opted for this, except that it seems to be the standard in other Lisps. But that's something I'd have done differently.

Backlinks (2 items)