Skip to content

Instantly share code, notes, and snippets.

@vvvvalvalval
Last active February 27, 2024 15:49
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save vvvvalvalval/f1250cec76d3719a8343 to your computer and use it in GitHub Desktop.
Save vvvvalvalval/f1250cec76d3719a8343 to your computer and use it in GitHub Desktop.
Asynchronous error management in Clojure(Script)
;; Synchronous Clojure trained us to use Exceptions, while asynchronous JavaScript has trained us to use Promises.
;; In contexts where we work asynchronously in Clojure (in particular ClojureScript), it can be difficult to see a definite way of managing failure. Here are some proposals.
;; OPTION 1: adapting exception handling to core.async CSPs
;; As proposed by David Nolen, with some macro sugar we use Exceptions in go blocks with core async in the same way we would do with synchronous code.
(require '[clojure.core.async :as a :refer [go]])
;; defining some helper macros
(defn throw-err "Throw if is error, will be different in ClojureScript"
[v]
(if (isa? java.lang.Throwable v) (throw v) v))
(defmacro <? "Version of <! that throw Exceptions that come out of a channel."
[c]
`(throw-err (a/<! ~c)))
(defmacro err-or "If body throws an exception, catch it and return it"
[& body]
`(try
~@body
(catch [java.lang.Throwable e#] e#)))
(defmacro go-safe [& body]
`(go (err-or ~@body)))
;; examples
(go
(try
(let [v1 (a/<? (dangerous-op-1))
v2 (a/<? (dangerous-op-2 v1))
v3 (a/<? (dangerous-op-3 v1 v2))]
(make-something-of v1 v2 v3))
(catch [Throwable e] (println "something wrong happened"))))
;; OPTION 2: using monads
;; Option 1 lets you deal with failure the same ways synchronous code does - with the Error type of the host platform.
;; Another approach, more akin to JavaScript Promises, is to use Monads.
;; The Cats library is one of several options for using monads in Clojure and ClojureScript.
;; What interests us here is the Exception monad type.
(require '[cats.core :as cats :refer [mlet]])
(require '[cats.monad.exception :as exc])
;; An Exception monadic value can be either a Success of some data, or a Failure of some error.
(def mv1 (exc/success 42))
mv1 ; => #<Success@3efd25fd: 42>
(def mv2 (exc/failure {:reason "no can do."}))
(type mv2) ; => cats.monad.exception.Failure
;; Monads are most interesting used with the mlet macro, which lets you write code in a world withour errors:
(mlet [v1 (exc/success 42)
v2 (exc/failure {:reason "no can do"})
v3 (exc/success 23)
v4 (+ v1 v3)]
(+ v1 v4))
;; The thing with monads is... they're not very compatible with core.async's go blocks.
;; Monads are about using functions to transform values, hence the mlet macro expands to a lot of nested functions.
;; But since the inversion of control provided by go blocks stops at function boundary, you typically can't mix mlet and <! or >!.
;; So assuming that the `dangerous-op-*` yield Exception Monad values, the following will not compile:
(declare dangerous-op-1 dangerous-op-2 dangerous-op-3)
(go
(mlet [v1 (a/<! (dangerous-op-1))
v2 (a/<! (dangerous-op-2 v1))
v3 (a/<! (dangerous-op-3 v1 v2))]))
; => IllegalArgumentException No method in multimethod '-item-to-ssa' for dispatch value: :fn clojure.lang.MultiFn.getFn (MultiFn.java:160)
;; However, making the Exception Monad work nicely with `go` is not a lost cause.
;; We can write our own let-like macro that expands to matching on the type of the monadic values, on which the `go` macro can operate its magic.
;; Below is a simplistic implementation:
(defmacro exc-let "Like let, but assumes that all the right hand expressions in the binding forms will evaluate to an Exception monadic value.
The left-expression in the bindings forms will be bound to the wrapped value if successful, otherwise the failure will be returned instead.
You may want to use it instead of cats.core/mlet inside of clojure.core.async/go blocks."
[bindings & body]
(->> bindings (partition 2) reverse
(reduce
(fn [inner [l r]]
`(let [l# ~r]
(cond
(exc/failure? l#) l#
(exc/success? l#) (let [~l (exc/extract l#)] ~inner)
)))
`(do ~@body)
)))
;; Example:
(go
(exc-let [v1 (a/<! (a/go (exc/success 42)))
v2 (a/<! (a/go (exc/failure {:reason "no can do."})))
v3 (a/<! (a/go (exc/try-on (+ v1 23))))]
(+ v1 v2 v3)))
;; OPTION 2-bis: using maps
;; Instead of a monadic type, you can use plain clojure maps with the following schema
{:outcome :success :data 42} ; successful value
{:outcome :error :data "no can do."} ; failed value
;; Depending on your situation this may be more portable; it also has the advantage that you can use core.match on it out of the box
(require '[clojure.core.match :refer [match]])
(defn log-outcome! [v]
(match [v]
[{:outcome :success :data data}] (prn "I succeeded!" data)
[{:outcome :error :data err}] (prn "I failed..." err)))
(log-outcome! {:outcome :success :data 42})
(log-outcome! {:outcome :error :data "no can do."})
;; OPTION 3: using Promises
;; If you're in ClojureScript, and not interested in core.async, you can just use a Promise library:
;; - funcool/promesa is a ClojureScript wrapper of the popular Bluebird JavaScript library.
;; - jamesmacaulay/cljs-promises is a Promise library designed to operate nicely with core.async.
;; Promises take care of both asynchrony and error management (they're essentially a mix of Futures and Exception Monads); some may say it's convenient, others may argue it's not simple.
@marco-m
Copy link

marco-m commented Aug 16, 2018

Are you aware of https://github.com/alexanderkiel/async-error ? That is nice, lightweight and explicit. From the README example:

(defn read-both [ch-a ch-b]
  (go-try
    (let [a (<? ch-a)
          b (<? ch-b)]
      [a b])))

This function returns a channel conveying either a vector of a and b or one of the errors conveyed by ch-a or ch-b. It will never read from ch-b if ch-a returns an error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment