Every language
11 lenguajes, copy-ready. One at a time with syntax highlighting, or all inline.
JSJavaScript
async function fetchWithRetry(url, attempts = 5, baseMs = 200) {
for (let attempt = 0; ; attempt++) {
try {
const res = await fetch(url);
if (res.ok) return res;
// 429 and 5xx may recover; other 4xx never will
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || attempt === attempts - 1) {
const err = new Error(`HTTP ${res.status}`);
err.retryable = retryable;
throw err;
}
} catch (err) {
const last = attempt === attempts - 1;
if (last || err.retryable === false) throw err;
}
const jitter = Math.random() * baseMs; // stops retries marching in lockstep
await new Promise((r) => setTimeout(r, 2 ** attempt * baseMs + jitter));
}
}Network faults (fetch rejects with TypeError) have no retryable flag — undefined means retry. Honor Retry-After: parse res.headers.get('retry-after') and sleep that long when present.
TSTypeScript
interface RetryOptions {
attempts?: number;
baseMs?: number;
}
class HttpError extends Error {
constructor(readonly status: number, readonly retryable: boolean) {
super(`HTTP ${status}`);
}
}
export async function fetchWithRetry(
url: string,
{ attempts = 5, baseMs = 200 }: RetryOptions = {},
): Promise<Response> {
for (let attempt = 0; ; attempt++) {
try {
const res = await fetch(url);
if (res.ok) return res;
throw new HttpError(
res.status,
res.status === 429 || res.status >= 500,
);
} catch (err) {
if (attempt === attempts - 1 || (err instanceof HttpError && !err.retryable)) {
throw err; // out of attempts, or a permanent 4xx
}
const jitter = Math.random() * baseMs;
await new Promise((r) => setTimeout(r, 2 ** attempt * baseMs + jitter));
}
}
}The typed HttpError carries the retryable verdict — callers can branch on it. Network faults (TypeError from fetch) fall into the retry path.
GoGo
func getWithRetry(ctx context.Context, url string, attempts int, base time.Duration) (*http.Response, error) {
for attempt := 0; ; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
retryable := err != nil || res.StatusCode == http.StatusTooManyRequests || res.StatusCode >= 500
if !retryable || attempt == attempts-1 {
return res, err // success, permanent 4xx, or the final attempt
}
if res != nil {
res.Body.Close() // leak the failed body or exhaust the pool
}
jitter := time.Duration(rand.Int64N(int64(base))) // math/rand/v2
delay := base<<attempt + jitter
select {
case <-time.After(delay):
case <-ctx.Done(): // cancellation beats retrying
return nil, ctx.Err()
}
}
}base<<attempt is the exponential in one shift. The select makes the retry loop cancellable — without ctx a killed service keeps retrying.
RsRust
use rand::Rng;
use std::time::Duration;
async fn get_with_retry(
url: &str,
attempts: u32,
base: Duration,
) -> Result<String, reqwest::Error> {
let mut attempt = 0;
loop {
attempt += 1;
match reqwest::get(url).await.map(|r| r.error_for_status()) {
Ok(Ok(res)) => return res.text().await,
Ok(Err(err)) | Err(err) => {
if attempt >= attempts
|| err.status().is_some_and(|s| s.is_client_error())
{
return Err(err); // out of attempts, or a permanent 4xx
}
}
}
let jitter = rand::rng().random_range(0..base.as_millis() as u64);
tokio::time::sleep(base * 2u32.saturating_pow(attempt - 1) + Duration::from_millis(jitter)).await;
}
}error_for_status() folds 4xx/5xx into Err so one match handles both worlds; err.status() then separates permanent (client error) from retryable. rand 0.9 API.
PHPPHP
function getWithRetry(string $url, int $attempts = 5, int $baseMs = 200): string {
for ($attempt = 0; ; $attempt++) {
$body = @file_get_contents($url, false, stream_context_create([
'http' => ['timeout' => 5, 'ignore_errors' => true],
]));
if ($body !== false) {
return $body; // production: also inspect the status line
}
if ($attempt === $attempts - 1) {
throw new RuntimeException("GET $url failed after $attempts attempts");
}
// 2^attempt backoff + full-range jitter, in microseconds
usleep((2 ** $attempt) * $baseMs * 1000 + random_int(0, $baseMs) * 1000);
}
}random_int is cryptographic — overkill for jitter but the always-safe choice in PHP; mt_rand is the lighter alternative.
PyPython
import random
import time
import httpx
def get_with_retry(url: str, attempts: int = 5, base: float = 0.2) -> httpx.Response:
for attempt in range(attempts):
try:
response = httpx.get(url, timeout=5.0)
except httpx.TransportError:
if attempt == attempts - 1:
raise
else:
retryable = response.status_code == 429 or response.status_code >= 500
if not retryable or attempt == attempts - 1:
return response
time.sleep(base * 2 ** attempt + random.uniform(0, base))
raise AssertionError('unreachable')TransportError is the network-fault family (connect, read, write); raise_for_status() is deliberately NOT used — it would erase the status we need to classify.
C#C#
async Task<string> GetWithRetry(string url, int attempts = 5, double baseSec = 0.2)
{
using var client = new HttpClient();
for (var attempt = 0; ; attempt++)
{
try
{
var response = await client.GetAsync(url);
var retryable = response.StatusCode == HttpStatusCode.TooManyRequests
|| (int)response.StatusCode >= 500;
if (!retryable || attempt == attempts - 1)
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
catch (HttpRequestException) when (attempt < attempts - 1)
{
// network fault — fall through to the delay
}
var delay = TimeSpan.FromSeconds(
baseSec * Math.Pow(2, attempt) + Random.Shared.NextDouble() * baseSec);
await Task.Delay(delay);
}
}Polly is the production resilience library: Policy.Handle<HttpRequestException>().WaitAndRetryAsync(...) — reach for it outside exercises.
JvJava
static String getWithRetry(URI uri, int attempts, Duration base)
throws IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
for (int attempt = 0; ; attempt++) {
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(5)).GET().build();
try {
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
boolean retryable = response.statusCode() == 429
|| response.statusCode() >= 500;
if (!retryable || attempt == attempts - 1) {
return response.body();
}
} catch (IOException e) { // network fault — retryable
if (attempt == attempts - 1) throw e;
}
long jitter = ThreadLocalRandom.current().nextLong(base.toMillis());
Thread.sleep(base.multipliedBy(1L << attempt).plusMillis(jitter).toMillis());
}
}1L << attempt is the exponential. Resilience4j is the production library (Retry.of with IntervalFunction.ofExponentialRandomBackoff).
SwSwift
func getWithRetry(url: URL, attempts: Int = 5, base: TimeInterval = 0.2) async throws -> Data {
var attempt = 0
while true {
attempt += 1
do {
var request = URLRequest(url: url)
request.timeoutInterval = 5
let (data, response) = try await URLSession.shared.data(for: request)
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
let retryable = code == 429 || code >= 500
if !retryable || attempt == attempts { return data }
} catch {
if attempt == attempts { throw error } // network fault, last try
}
let jitter = Double.random(in: 0..<base)
try await Task.sleep(for: .seconds(base * pow(2, Double(attempt - 1)) + jitter))
}
}Task.sleep(for:) needs iOS 16/macOS 13. Parse the Retry-After header when present — it overrides any computed delay.
KtKotlin
import kotlinx.coroutines.delay
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import kotlin.random.Random
suspend fun getWithRetry(url: String, attempts: Int = 5, baseMs: Long = 200): String {
val client = HttpClient.newBuilder().build()
var attempt = 0
while (true) {
attempt++
val request = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofSeconds(5)).GET().build()
try {
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
val retryable = response.statusCode() == 429 || response.statusCode() >= 500
if (!retryable || attempt == attempts) return response.body()
} catch (e: IOException) {
if (attempt == attempts) throw e
}
delay((baseMs shl (attempt - 1)) + Random.nextLong(baseMs)) // jittered backoff
}
}delay (not Thread.sleep) keeps the coroutine off the thread while backing off — the whole point of suspend.
RbRuby
require 'net/http'
require 'uri'
def get_with_retry(url, attempts: 5, base: 0.2)
uri = URI(url)
attempts.times do |attempt|
response = Net::HTTP.get_response(uri)
retryable = response.code == '429' || response.code.to_i >= 500
return response if !retryable || attempt == attempts - 1
sleep(base * 2**attempt + rand * base) # full jitter
end
endNet::HTTP.get_response raises on network faults — wrap in begin/rescue retry when connection errors need the same treatment.