Welcome! Please see the About page for a little more info on how this works.

0 votes
in Errors by

Currently, clojure.core/ex-info must take a msg and data map, and may take a cause, which must be another Throwable, which may have its own cause, etc. etc..

This supports the case of "nested" exceptions quite well, e.g. in the case of compilation exceptions being caused by macroexpansion exceptions, potentially with their own cause, and works well for fail-on-first-problem situations.

There's also the situation of "lateral" exceptions, e.g. in the case of test runners or static analysers, which is not supported. In this case, if we take lazytest as a concrete example, the test runner throws an exception if any of the test cases it runs throws an exception, but the overall cause of the overall exception, conceptually, is all the individual exceptions.

Currently, the only to do this is to make some bespoke thing in the data map.

I propose changing clojure.core/ex-info to take [msg map & causes], which would be a non-breaking change (or, while we're at it, make the map arg optional as well and default it to {}, but that's a separate topic).

That leaves the question of how to handle ex-cause. To my understanding, to change it to return the cause or the list of causes, would maybe be a breaking change for general exception handling libraries or something, but would not imo pose any significant problem. Alternatively, we might have ex-causes to always return a list, and change ex-cause to (comp first ex-causes).

Any thoughts? I don't trust myself to spearhead a PR alone on this but I'd love to contribute, certainly the pure-clojure side of things seems straightforward enough (and I'd love to patch this into lazytest, but that's again a different topic).

1 Answer

+1 vote
by

I'm not sure this problem is common enough to warrant solving in core or extending the existing ex-info. If you are trying to throw a map bundling multiple exceptions, this really seems like a data problem more than an exception problem, and we have plenty of ways to bundle data into the ex map.

In Java, the typical solution for (one) lateral exception is to use ExecutionException, doesn't handle multiple though.

by
Hm, my thoughts were more along of a list of causes rather than a structured map bundling everything semantically. More along the lines of just an exception sometimes having more than one cause.

Imagine a linter-type situation. You almost never want to stop execution early, but at the end, either there's no problem, or there's is a (1) problem, which is that there are 1-or-more problems during linting.

Granted, a linter *could* just as well have used the ex-map (or really, no exceptions at all), but the test runner case is what sticks out to me. `lazytest` runs everything and returns a report with the number of exceptions, by order, commensurate to the failing test cases. In this case, all the semantics are already there, just a list of exceptions by order of reported failure would be perfect, but as it stands a semantic/bundling layer *must* be invented used and maintained, where one isn't necessary at all, so ExecutionException is not the right fit to my understanding.

I do see now that the `cause` construct goes down to Throwable, and isn't an ExceptionInfo-specific thing, but my proposed design still stands, insofar as a `causes` field can be introduced, wherefrom `cause` can be a getter.

Past the test runner use case, this can also be a bottleneck for code generation in particular, and some macro setups, and parallelism use cases, all of which right now could either (a) fail-first or (b) do bespoke wrappings and bundlings, introducing nesting and structure, where really (c) just having multiple causes on ExceptionInfo would've been good enough for all of these.

All of these aren't necessarily common problems, but there's really no way of addressing them in core besides bespoke messes and exception DSLs, and attempting to solve it outside of core could only work with more of the same. Maybe this could be solved with some ""canonical"" pattern in the docs to kinda standardise things a bit? But I don't see how this can be reconciled *strictly* from userland
by
I guess I don't follow your argument about LazyTest. For something to have a "list of exceptions", then it has already caught and accumulated all of those exceptions in a Clojure data structure -- so once it is "done", it could just (throw (ex-info "failures" {:causes list-of-caught-exes})) -- that data-conveyance is exactly what ex-info is intended for.

All exceptions in Java-land (including ex-info's ExceptionInfo) have .getCause as a method and Clojure's ex-cause is "just" a wrapper around that (that works across dialects too: ClojureScript's API and behavior here is essentially identical to Clojure's, just mapped to JS).

The work in dealing with the "list of exceptions" is in the domain-specific part of the code here: something has to catch & accumulate in a way that is appropriate for the domain.
...