Exception handling differences between Clojure map & pmap
With this post, I am in deeper waters than usual. What might sound like a recommendation in the following could be a potential disaster in disguise. Be warned.
Personally, I prefer not to know about implementation details about the function I’m calling. Although that was the situation I suddenly found myself in, when a function I call replaced map with pmap.
Here is how I approached the weirdness with exceptions tangled with pmap.
On the surface, map and pmap appear interchangeable, since they both return a lazy sequence. But the data contract breaks due to how exceptions are handled.
The following example showcases the behavior that caught me by surprise, because I had expected it to return {:error-code 42}:
(try
(->> (range 1)
(pmap (fn [_] (throw (ex-info "Oh noes" {:error-code 42}))))
doall)
(catch Exception e
(ex-data e)))
; => nil
It did not. But using a normal map does:
(try
(->> (range 1)
(map (fn [_] (throw (ex-info "Oh noes" {:error-code 42}))))
doall)
(catch Exception e
(ex-data e)))
; => {:error-code 42}
doall is necessary to ensure the exception is triggered while inside the try-catch block, instead of just returning an (unrealized) lazy sequence, which will cause havoc later.
As far as I know, pmap uses futures somewhere behind the scenes, which might be the reason why exceptions caused during mapping are wrapped in a java.util.concurrent.ExecutionException.
Since I am in control of the function replacing map with pmap, I decided to put the unwrapping where pmap is called, to hide the implementation detail from the caller:
(try
(->> coll
(pmap #(occasionally throw-exception %))
doall)) ; realize lazy seq to trigger exceptions
(catch Exception e
; Unwrap potentially wrapped exception by `pmap`
(throw (if (instance? java.util.concurrent.ExecutionException e)
(ex-cause e)
e)))))
The conditional unwrapping allows for a slightly more complex implementation in the try block that can throw exceptions outside pmap as well.
The above implementation assumes that an ExecutionException always has a cause, which might not be the case - I don’t know.
Use with caution.