{"slug": "two-tiny-functions-that-make-your-async-code-production-ready-retry-and-timeout", "title": "Two tiny functions that make your async code production-ready: `retry` and `timeout`", "summary": "The article explains two concise higher-order functions, `retry` and `timeout`, that make async JavaScript code more resilient in production environments. The `retry` function automatically reattempts a failed async operation a specified number of times, while the `timeout` function aborts any operation that exceeds a given time limit. Both functions wrap existing async functions without modifying their internal logic and can be composed together to handle network failures and slow responses.", "body_md": "Every async function you write assumes the network cooperates, the server responds, and the database doesn't hiccup. In production, none of those assumptions hold forever.\n\nHere are two higher-order functions — each under 15 lines — that make any async function resilient without touching its internals.\n\n## The problem\n\nYou have an async function. Maybe it calls an API, queries a database, or reads a file over the network.\n\n``` js\nasync function fetchUserData(userId) {\n    const response = await fetch(`/api/users/${userId}`);\n    return response.json();\n}\n```\n\nTwo things will go wrong eventually:\n\n- It will fail intermittently and you'll want to retry it\n- It will hang indefinitely and you'll want to give up after a deadline\n\nYou could wrap every function in retry logic and timeout logic inline. Or you could write it once, properly, and wrap any function you want.\n\n## `retry`\n\n— automatic reattempts on failure\n\n```\nexport function retry(count, callback) {\n  return async function (...args) {\n    let attempts = 0;\n    let lastError;\n\n    while (attempts <= count) {\n      try {\n        return await callback(...args);\n      } catch (err) {\n        lastError = err;\n        attempts++;\n      }\n    }\n\n    throw lastError;\n  };\n}\n```\n\n### How it works\n\n`retry`\n\nis a higher-order function — it takes a function and returns a new function with retry behaviour baked in. The original function is untouched.\n\nThe `while (attempts <= count)`\n\ncondition is deliberate. If `count`\n\nis `3`\n\n, the loop runs when `attempts`\n\nis `0, 1, 2, 3`\n\n— that's 4 total executions: one initial attempt plus three retries. This matches the natural language meaning of \"retry 3 times\".\n\nOn success, `return await callback(...args)`\n\nexits immediately — no more iterations. On failure, the error is stored in `lastError`\n\nand `attempts`\n\nincrements. Once the loop exhausts all attempts, the last error is rethrown — not a generic `new Error('Max retries reached')`\n\n, but the actual error the callback produced. Your callers get a meaningful error message, not a wrapper.\n\n### Usage\n\n``` js\nconst resilientFetch = retry(3, fetchUserData);\n\n// Works exactly like fetchUserData, but retries up to 3 times on failure\nconst user = await resilientFetch('user_123');\n```\n\n### Why `await`\n\ninside `try`\n\nmatters\n\n```\ntry {\n    return await callback(...args); // ✓ catches rejected promises\n} catch (err) { ... }\n```\n\nWithout `await`\n\n, a rejected promise escapes the try/catch entirely:\n\n```\ntry {\n    return callback(...args); // ✗ returns a pending promise — catch never fires\n} catch (err) { ... }\n```\n\n`await`\n\nunwraps the promise inside the try block, so rejections are catchable. This is one of the most common async/await mistakes and `retry`\n\nonly works correctly because it gets this right.\n\n## `timeout`\n\n— give up after a deadline\n\n```\nexport function timeout(delay, callback) {\n  return async function (...args) {\n    const timer = new Promise((_, reject) =>\n      setTimeout(() => reject(new Error('timeout')), delay)\n    );\n\n    return Promise.race([callback(...args), timer]);\n  };\n}\n```\n\n### How it works\n\n`Promise.race`\n\nresolves or rejects with whichever promise settles first. This function creates a race between two competitors:\n\n-\n`callback(...args)`\n\n— the actual work -\n`timer`\n\n— a promise that rejects after`delay`\n\nmilliseconds\n\nIf the callback finishes in time, its value wins and `timer`\n\nbecomes irrelevant. If `delay`\n\nmilliseconds pass first, `timer`\n\nrejects with `Error('timeout')`\n\nand the callback's eventual result is ignored.\n\nNotice the timer promise is constructed with `(_, reject)`\n\n— it never resolves, only rejects. This ensures the timer can never accidentally win the race with a successful value; it can only interrupt with a failure.\n\n### Usage\n\n``` js\nconst limitedFetch = timeout(5000, fetchUserData);\n\ntry {\n    const user = await limitedFetch('user_123');\n} catch (e) {\n    if (e.message === 'timeout') {\n        console.error('Request took too long');\n    }\n}\n```\n\n## Combining them\n\nBoth functions return async functions with the same signature as their input — which means they compose cleanly.\n\n``` js\n// Retry up to 3 times, but abandon any single attempt after 5 seconds\nconst resilientFetch = retry(3, timeout(5000, fetchUserData));\n\nawait resilientFetch('user_123');\n```\n\nHere's what happens on each attempt:\n\n-\n`timeout(5000, fetchUserData)`\n\nraces the fetch against a 5-second timer - If it times out,\n`timeout`\n\nrejects with`Error('timeout')`\n\n-\n`retry`\n\ncatches that rejection, increments attempts, and tries again - After 3 retries all fail,\n`retry`\n\nrethrows the last error\n\nFour attempts, each with a 5-second ceiling, maximum 20 seconds total. All from two composable functions and one line of setup.\n\n## What makes these worth keeping\n\n**They don't modify the original function.** `fetchUserData`\n\nis unchanged. You can use it with or without retry/timeout anywhere else.\n\n**They forward arguments transparently.** `...args`\n\npasses everything through — the wrapped function behaves identically to the original from the caller's perspective.\n\n**They preserve the error.** `retry`\n\nrethrows `lastError`\n\n, not a new generic error. `timeout`\n\nrejects with a named `Error('timeout')`\n\nyou can check by message. Callers always know what actually went wrong.\n\n**They compose.** Because both return async functions with matching signatures, you can layer them in any order and they work together without knowing about each other.\n\n## The pattern\n\nBoth functions follow the same structure:\n\n```\nhigherOrderFn(config, callback) {\n    return async function (...args) {\n        // enhanced behaviour around callback(...args)\n    }\n}\n```\n\nThis is the decorator pattern applied to async functions. You write the enhancement once, and apply it to any async function that needs it — no inheritance, no classes, no modification of the original. Just functions wrapping functions.\n\nIt's a small pattern. It shows up everywhere once you start looking for it.", "url": "https://wpnews.pro/news/two-tiny-functions-that-make-your-async-code-production-ready-retry-and-timeout", "canonical_source": "https://dev.to/danikeya/two-tiny-functions-that-make-your-async-code-production-ready-retry-and-timeout-44e3", "published_at": "2026-05-24 00:01:00+00:00", "updated_at": "2026-05-24 00:33:47.982870+00:00", "lang": "en", "topics": ["developer-tools"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/two-tiny-functions-that-make-your-async-code-production-ready-retry-and-timeout", "markdown": "https://wpnews.pro/news/two-tiny-functions-that-make-your-async-code-production-ready-retry-and-timeout.md", "text": "https://wpnews.pro/news/two-tiny-functions-that-make-your-async-code-production-ready-retry-and-timeout.txt", "jsonld": "https://wpnews.pro/news/two-tiny-functions-that-make-your-async-code-production-ready-retry-and-timeout.jsonld"}}