Search code examples
error-handlingclojure

clojure threading macro with failure form


I have a few operations I want to thread where each can fail. I would much rather get the error as a value instead of using try-catch which breaks the flow of execution.

I can do the naive version and make my functions use nil as failure:

(if-let (op1 ...)
  (if-let (op2 ...)
    ...
    err1)
  err2)

but this is nested and makes it harder to read.

I could use some-> which seems like the closest solution but it doesn't say what failed:

(if-let [res (some-> arg
                     op1
                     op2)]
  res
  somethin-failed) ;; what failed though?

I also looked at ->, and cond-> but they don't seem to help.

I know there are macros online to do these kind of things but I would much rather not add macros if something exists to solve this. Hopefully there is something of the form:

(some-with-err-> arg
                 op1 err1
                 op2 err2
                 ...)

I may be overlooking something simpler, but I can't seem to find something built-in to address this issue.

I can write a macro to do it but would rather avoid it for now.


Solution

  • There's nothing built-in for this, but there are libraries for monadic error handling (e.g. Failjure) which seems like what you're looking for.

    You could derive a version some-with-err-> from the some-> macro definition. The only practical difference is the map function that binds to steps now partitions the forms/error values, wraps step invocations in try and returns a namespaced map on failure:

    (defmacro some-with-err->
      [expr & forms]
      {:pre [(even? (count forms))]}
      (let [g (gensym)
            steps (map (fn [[step error]]
                         `(if (or (nil? ~g) (::error ~g))
                            ~g
                            (try (-> ~g ~step)
                                 (catch Exception _# {::error ~error}))))
                       (partition 2 forms))]
        `(let [~g ~expr
               ~@(interleave (repeat g) (butlast steps))]
           ~(if (empty? steps)
              g
              (last steps)))))
    

    It can be used like some-> but each form must be accompanied by an error return value:

    (some-with-err-> 1
      (+ 1) :addition
      (/ 0) :division
      (* 2) :multiplication)
    => #:user{:error :division}
    
    (some-with-err-> " "
      (clojure.string/trim) :trim
      (not-empty) :empty
      (str "foo") :append)
    => nil ;; from not-empty