In Ruby, how does retry behave inside a rescue clause, and how do you retry a payment-gateway call a bounded number of times?
answer
- back to the top of the body
- legal only inside rescue
- counter lives outside the begin
- ensure runs once, at the end
- rescue only transient classes
basics
~20 sretry, legal only inside a rescue clause, restarts the begin body (or method or do...end body) from its first line. Keep an attempt counter outside that body, retry while under a limit, then re-raise; ensure runs once, after the last attempt.
solid answer
~40 s`retry` jumps from a `rescue` clause back to the first statement of the protected body: the `begin` block, or the method or `do...end` block when the rescue is attached to it. It does not resume at the failing line, and it has no built-in limit, so the attempt counter must be initialised **before** the `begin`; a counter set inside the body is reset on every retry and the loop never ends. For a payment gateway: rescue only transient classes such as `Net::ReadTimeout`, `Net::OpenTimeout` or `Errno::ECONNRESET`, `retry` while `attempts < 3` (ideally after a short `sleep`), and otherwise re-raise. `ensure` does not run between attempts; it runs once when the block finally finishes, which is where the connection is closed. `retry` anywhere but a rescue clause is a `SyntaxError`.
code
ruby · 18 linesrequire "net/http"
def charge(order_id, cents)
http = Net::HTTP.new("gateway.example", 443)
http.use_ssl = true
attempts = 0 # outside the body: survives retry
begin
attempts += 1
http.start unless http.started?
http.post("/charges", "order=#{order_id}¢s=#{cents}").code == "201"
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET
raise if attempts >= 3
sleep(0.2 * attempts)
retry
ensure
http.finish if http.started? # runs once, after the last attempt
end
endgo deeper
Remember that retry reruns the begin body from the top, only works inside rescue, and needs a counter to stop.
Explain where the counter must live, why ensure runs only once, and why only transient exception classes should trigger a retry.
Discuss whether the operation is safe to repeat, backoff between attempts, idempotency keys for payments, and cleanup that cannot itself raise from ensure.
Decide where retry policy lives: inline retry for one call site, or a shared wrapper with consistent limits, backoff and logging across services.
## What retry does `retry` is a keyword that can appear only inside a **`rescue` clause**. It transfers control back to the **start of the protected body**: - for `begin ... rescue ... end`, the first statement after `begin`; - for a rescue attached to a method (`def ... rescue ... end`), the first statement of the method body; - for a rescue attached to a `do ... end` block, the first statement of the block body. Everything in the body runs again, including statements before the one that failed. Ruby adds no limit or delay; the syntax documentation warns you to be careful not to create an infinite loop. Using `retry` outside a rescue clause, or inside an `else` or `ensure` clause, is rejected as a `SyntaxError` when the file is parsed. ## Bounding the attempts A safe retry has five parts: 1. **Initialise the counter before `begin`.** A local assigned before the handler survives each `retry`; one assigned inside the body is reset every time. 2. **Increment it inside the body**, so each attempt counts. 3. **Rescue only transient failures.** Timeouts and connection resets are worth another try; `ArgumentError` or `NoMethodError` will fail identically every time. 4. **Check the limit in the rescue clause.** `retry if attempts < 3`. 5. **Re-raise when the limit is hit**, so the caller sees the real error instead of a silent `nil`. A short `sleep` that grows with the attempt number, such as `sleep(0.2 * attempts)`, gives a struggling service time to recover. ## How ensure interacts with retry `retry` does not leave the `begin` block, so **`ensure` does not run between attempts**. It runs once, after the final attempt succeeds or the final exception is re-raised. That makes `ensure` the right place to close the gateway connection: one close, whatever happened. | Event | rescue runs | ensure runs | |---|---|---| | attempt 1 times out | yes, calls `retry` | no | | attempt 2 times out | yes, calls `retry` | no | | attempt 3 succeeds | no | yes, once | | attempt 3 times out | yes, re-raises | yes, once, then the error leaves | ## Retrying a payment call safely In the snippet, the connection is opened inside the body only if it is not already started, so a retry after a dropped connection reconnects, while the `ensure` closes it once at the end. Two details deserve attention in an interview: - A read timeout on a charge does not mean the charge failed. The gateway may have processed it and the reply was lost. Retrying a non-idempotent request can double-charge, so real integrations send an **idempotency key** the gateway uses to recognise the repeat. - Net::HTTP's `finish` raises `IOError` when the session was never started, which is why the `ensure` checks `started?` first. An exception raised from `ensure` would replace the error you were trying to report. ## Common bugs - `attempts = 0` written inside `begin`: every retry resets it, so the loop never ends. - A bare `rescue` with `retry`: a typo that raises `NoMethodError` is retried until the limit, delaying the real error. - No backoff: three attempts in a few milliseconds against an overloaded service rarely help. - Expecting `retry` to resume at the failing line: side effects earlier in the body run again, so they must be safe to repeat. ## retry versus a loop Some teams prefer an explicit loop, `3.times do ... end` with a `break` on success, because the attempt count is visible in the loop header. Both work; `retry` keeps the happy path unindented and puts the retry decision next to the error class that justifies it. Whichever you pick: - keep the limit and the delay in one place, ideally constants; - log each failed attempt with its number, so a slow gateway shows up in monitoring before it becomes an outage; - re-raise the original exception after the last attempt instead of wrapping it in a new, less specific one. In an interview, walking through the counter, the class list, the backoff and the single `ensure` is usually what distinguishes an answer that has been used in production from one read in a book.
- Why must the attempt counter be initialised outside the begin block?`retry` restarts the protected body from its first line. If `attempts = 0` is inside the body, it runs again on every retry, the counter never passes 1 and the limit check never fires, so the call is retried forever. Assigned before `begin`, the local keeps its value across retries.
- Does ensure run after each failed attempt?No. `retry` stays inside the same `begin` block, so control never leaves it between attempts. `ensure` runs once, when the block finally completes or the last exception propagates. Per-attempt cleanup has to be written in the rescue clause before `retry`.
- Where is retry legal, and what happens elsewhere?Only inside a `rescue` clause, whether that clause belongs to a `begin` block, a method body or a `do...end` block. Writing `retry` in the body, in `else`, in `ensure` or outside any handler is a `SyntaxError` reported when the file is parsed, not a runtime error.
Like redialling a busy number: every attempt starts again from the first digit, not from where the line dropped, and you tally attempts on a note kept beside the phone, not on one you rewrite each time you dial.
saying these in an interview costs you the question
- retry resumes execution at the line that raised
- retry gives up by itself after a few attempts
- ensure runs after every failed attempt
- retry may be used anywhere inside a begin block
- Resetting the counter at the top of the begin body is fine
- Any exception is worth retrying with a bare rescue