One rough edge in pedestal is propagating errors from async interceptors.
Sync interceptors can throw exceptions and expect them to be caught by their or a predecessor’s (next in the leave queue) :error function, just as documented in the https://pedestal.io/pedestal/0.8/reference/error-handling.html.
Async interceptors cannot throw exceptions, they must return a context map. You might think they could kick off the error phrase by returning a context map with the :io.pedestal.interceptor.chain/error entry that is normally added by pedestal after an exception is thrown by an interceptor. After all the error handling reference states, “As long as there is an error attached to that key, Pedestal will not invoke the usual :enter and :leave functions.” But that is incorrect.
My testing and a close read of the code shows that if you attach this key and an appropriate value to a context map returned out of a channel from an interceptor phase fn — an :enter fn specifically, as I’ll explain — pedestal will happily continue on to the next interceptor in that phase chain. Not surprising: The fn to make the context enter the error phase is chain/begin-error and that is called by chain/try-stage only when an actual exception is thrown by an interceptor fn (“callback”). When an interceptor goes async, chain/process-async-context is called and does a rough approximation of try-stage but again only checks for a thrown exception, and unlike try-stage the only code it is executing, the only thing that could throw an exception to catch, is its own, not anything from the interceptor which ran its code in a go or thread.
Even in sync code you have to throw an exception; there is nothing that stops the enter phase on the presence of the ::chain/error key. (The leave phase, due to particulars of its implementation, will indeed effectively short circuit into error phase if that key is there; the error phase is basically implemented as a branch of the leave phase.)
If you’re interested in making the behavior match that line in the docs I’d considering adding a “terminator” for the ::chain/error key the way you do for the :response key; terminators /are/ called both by try-stage and process-async-content.
I’d also consider going further: Allow interceptor fn channels to return either a context map or an exception object, check for the exception object in process-async-context (or another function you put in front of it in the go-async thread) , and begin-error the ex into the context to start the error phase. Accepting exception objects would be a nice affordance because it’s kind of a bear for the user to make an appropriate value to go with the ::chain/error key, all the helper fns are private and just from the context you can’t extract for example the interceptor name (in the leave phase at least) so you end up repeating yourself in the interceptor code.
Anyway, for now I am solving the problem with these, you can see how they require you to repeat your interceptor name and such:
(require '[io.pedestal.interceptor.chain :as chain])
;;useful when you catch an exception yourself and just want to bubble
;;it out of your (async) interceptor instead of handling it there
(defn error-phase
[context interceptor-name ex]
(-> context
(dissoc ::chain/queue)
(assoc ::chain/error
((var chain/throwable->ex-info)
ex
(context ::chain/execution-id)
interceptor-name
(if (context ::chain/stack)
:enter
:leave)))))
;;useful replacement for go` in interceptor fns - will bubble`
;;exceptions as though they happened in a sync interceptor fn
(defmacro go-error-into
[context interceptor-name & body]
(go`
(try ~@body
(catch Exception e#
(error-phase ~context ~interceptor-name e#)))))
These also know a little much about the internals of pedestal for my liking. Not just the private fn prying but clearing the queue, which is basically an implementation detail of error bubbling.
If you’re open to a patch just let me know, although I imagine you may want to hammock how to handle this which is obviously fine too.
(There was a https://clojurians.slack.com/archives/C0K65B20P/p1589739017125800.)
(Somewhat strangely, the Error Handling reference page states, “Pedestal unifies error handling for synchronous interceptors and asynchronous interceptors.” I am not sure what this refers to but it’s not really true at all. In the enter phase, error handling is exception driven, and so async cannot participate, at least without going to the lengths of my fns above)