44

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))))
Jonas
  • 19,422
  • 10
  • 54
  • 67
GabiMe
  • 18,105
  • 28
  • 76
  • 113

8 Answers8

47

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)))
kotarak
  • 17,099
  • 2
  • 49
  • 39
  • This is a fine solution. I would add it to clojure.contrib or something. – GabiMe Dec 10 '09 at 10:26
  • It's actually the same solution as the one the poster suggested. But macros make it easier to do in the general case. Macros are the killer feature of any lisp variant. – Jeremy Wall Dec 10 '09 at 18:17
  • It's not exactly the same solution. The poster's suggestion does not catch the return value of the block, and if it did the block wouldn't be able to return nil. Also the exceptions are swallowed. But you are right: it's basically the same idea. Macros just hide the boilerplate. – kotarak Dec 10 '09 at 21:48
  • Doesn't the macro save nothing more than a `#` for the caller to create the thunk? Is it idiomatic to use macros in clojure just to save a `#`? – Dax Fohl Nov 25 '13 at 15:01
  • 4
    Can I ask you why you put the result of the thunk in a vector? I'm not sure why you can't just put it as a "naked" value? – Christophe De Troyer Dec 17 '14 at 10:09
  • 4
    @ChristopheDeTroyer otherwise if (thunk) returns nil, it would be treated as a false result of the if-let – noisesmith Dec 17 '14 at 16:24
  • One feature I'd regard as important for a generic solution is an exception "filter" so that the caller can choose which exceptions cause a retry and which should instantly abort (e.g. retry on database transaction deadlock, but abort on "table does not exist") – Andy Feb 11 '21 at 20:03
13

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} ...)))
amalloy
  • 89,153
  • 8
  • 140
  • 205
9

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))))
Evgeniy Berezovsky
  • 18,571
  • 13
  • 82
  • 156
4

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))
defhlt
  • 1,775
  • 2
  • 17
  • 24
0

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))))))))
SerCe
  • 5,826
  • 2
  • 32
  • 53
0

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

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))))
piercebot
  • 1,767
  • 18
  • 16
0

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))
Didier A.
  • 4,609
  • 2
  • 43
  • 45