Clojure: How to to recur upon exception?

Viewed 7005

I am trying to execute a func several times before giving up upon exceptions. But it is not valid in Clojure to recur from catch block. How can this be achieved ?

(loop [tries 10]
  (try
    (might-throw-exception)
    (catch Exception e
      (when (pos? tries) (recur (dec tries))))))

java.lang.UnsupportedOperationException: Cannot recur from catch/finally 

The best I could find is the following clumsy solution (wrapping in func and calling it)

(defn do-it []
  (try
    (might-throw-exception)
    (catch Exception e nil)))

(loop [times 10]
  (when (and (nil? (do-it)) (pos? times))
    (recur (dec times))))
8 Answers

This allows catching multiple more then one exception and provides some feedback about the causes for the retries.

(defmacro try-n-times
  "Try running the body `n` times, catching listed exceptions."
  {:style/indent [2 :form :form [1]]}
  [n exceptions & body]
  `(loop [n# ~n
          causes# []]
     (if (> n# 0)
       (let [result#
             (try
               ~@body
               ~@(map (partial apply list 'catch) exceptions (repeat `(e# e#))))]
         (if (some #(instance? % result#) ~exceptions)
           (recur (dec n#) (conj causes# result#))
           result#))
       (throw (ex-info "Maximum retries exceeded!"
                       {:retries ~n
                        :causes causes#})))))

If you add a result arg to your loop, you can nest the (try) block inside of the (recur). I solved it like this:

(loop [result nil tries 10]
  (cond (some? result) result
        (neg? tries) nil
        :else (recur (try (might-throw-exception)
                          (catch Exception e nil))
                     (dec tries))))

Here's yet another approach:

(loop [tries 10]
  (let [res (try
              (might-throw-exception)
              (catch Exception e
                (if (pos? tries)
                  ::retry
                  (throw e))))]
    (if (#{::retry} res)
      (recur (dec tries))
      res)))

But may I also recommend a cool little trick, instead of having a number of retries, provide a seq of times to sleep for:

(loop [tries [10 10 100 1000]]
  (let [res (try
              (might-throw-exception)
              (catch Exception e
                (if tries
                  ::retry
                  (throw e))))]
    (if (#{::retry} res)
      (do
        (Thread/sleep (first tries))
        (recur (next tries)))
      res)))

And finally put it all into a macro if you want it to be less verbose:

(defmacro with-retries
  [retries & body]
  `(loop [retries# ~retries]
     (let [res# (try ~@body
                     (catch Exception e#
                       (if retries#
                         'retry#
                         (throw e#))))]
       (if (= 'retry# res#)
         (do (Thread/sleep (first retries#))
             (recur (next retries#)))
         res#))))

(with-retries [10 10 100 1000]
  (might-throw-exception))
Related