42
votes

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

8 Answers

45
votes

Macros are calling...

How about this:

(defn try-times*
  "Executes thunk. If an exception is thrown, will retry. At most n retries
  are done. If still some exception is thrown it is bubbled upwards in
  the call chain."
  [n thunk]
  (loop [n n]
    (if-let [result (try
                      [(thunk)]
                      (catch Exception e
                        (when (zero? n)
                          (throw e))))]
      (result 0)
      (recur (dec n)))))

(defmacro try-times
  "Executes body. If an exception is thrown, will retry. At most n retries
  are done. If still some exception is thrown it is bubbled upwards in
  the call chain."
  [n & body]
  `(try-times* ~n (fn [] ~@body)))
12
votes

kotarak's idea is the way to go, but this question tickled my fancy so I'd like to provide a riff on the same theme that I prefer because it doesn't use loop/recur:

(defn try-times* [thunk times]
  (let [res (first (drop-while #{::fail}
                               (repeatedly times
                                           #(try (thunk)
                                                 (catch Throwable _ ::fail)))))]
    (when-not (= ::fail res)
      res)))

And leave the try-times macro as it is.

If you want to allow the thunk to return nil, you can drop the let/when pair, and let ::fail represent "the function failed n times", while nil means "the function returned nil". This behavior would be more flexible but less convenient (the caller has to check for ::fail to see if it worked rather than just nil), so perhaps it would be best implemented as an optional second parameter:

(defn try-times* [thunk n & fail-value]
  (first (drop-while #{fail-value} ...)))
7
votes

A try-times macro is elegant, but for a one-off, just pull your when out of the try block:

(loop [tries 10]
  (when (try
          (might-throw-exception)
          false ; so 'when' is false, whatever 'might-throw-exception' returned
          (catch Exception e
            (pos? tries)))
    (recur (dec tries))))
4
votes

My proposal:

(defmacro try-times
  "Retries expr for times times,
  then throws exception or returns evaluated value of expr"
  [times & expr]
  `(loop [err# (dec ~times)]
     (let [[result# no-retry#] (try [(do ~@expr) true]
                   (catch Exception e#
                     (when (zero? err#)
                       (throw e#))
                     [nil false]))]
       (if no-retry#
         result#
         (recur (dec err#))))))

Will print "no errors here" once:

(try-times 3 (println "no errors here") 42)

Will print "trying" 3 times, then throw Divide by zero:

(try-times 3 (println "trying") (/ 1 0))
0
votes

One more solution, without macro

(defn retry [& {:keys [fun waits ex-handler]
                :or   {ex-handler #(log/error (.getMessage %))}}]
  (fn [ctx]
    (loop [[time & rem] waits]
      (let [{:keys [res ex]} (try
                               {:res (fun ctx)}
                               (catch Exception e
                                 (when ex-handler
                                   (ex-handler e))
                                 {:ex e}))]
        (if-not ex
          res
          (do
            (Thread/sleep time)
            (if (seq rem)
              (recur rem)
              (throw ex))))))))
0
votes

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#})))))
0
votes

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))))
0
votes

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))