{"id":"ea8e1dee78d23d6b","repo":"sindresorhus/got","slug":"err-retrying","errorCode":"ERR_RETRYING","errorMessage":"Retrying","messagePattern":"Retrying","errorType":"exception","errorClass":"RetryError","httpStatus":null,"severity":"info","filePath":"source/as-promise/index.ts","lineNumber":167,"sourceCode":"\t\t\t\t\t\t\t\t\t\t\toptions.stripUnchangedCrossOriginState(previousState!, changedState, {clearBody: !hasExplicitBody});\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\toptions.stripSensitiveHeaders(previousUrl, nextUrl, updatedOptions);\n\n\t\t\t\t\t\t\t\t\t\t\tif (!isSameOrigin(previousUrl, nextUrl) && !hasExplicitBody) {\n\t\t\t\t\t\t\t\t\t\t\t\toptions.clearBody();\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Remove any further hooks for that request, because we'll call them anyway.\n\t\t\t\t\t\t\t\t// The loop continues. We don't want duplicates (asPromise recursion).\n\t\t\t\t\t\t\t\t// Unless preserveHooks is true, in which case we keep the remaining hooks.\n\t\t\t\t\t\t\t\tif (!preserveHooks) {\n\t\t\t\t\t\t\t\t\toptions.hooks.afterResponse = options.hooks.afterResponse.slice(0, index);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tthrow new RetryError(request);\n\t\t\t\t\t\t\t}));\n\n\t\t\t\t\t\t\tif (!(is.object(response) && is.number(response.statusCode) && 'body' in response)) {\n\t\t\t\t\t\t\t\tthrow new TypeError('The `afterResponse` hook returned an invalid value');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\t\trequest._beforeError(normalizeError(error));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tglobalResponse = response;\n\n\t\t\t\t\tif (!isResponseOk(response)) {\n\t\t\t\t\t\trequest._beforeError(new HTTPError(response));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/sindresorhus/got/blob/e3924aa1e53a6ca3eb93a43618ce532442a89b40/source/as-promise/index.ts#L149-L185","documentation":"RetryError (code ERR_RETRYING) is an internal control-flow signal, not a real failure. It is thrown at source/as-promise/index.ts:167 inside the retry callback that an afterResponse hook invokes via its second argument (the `retry` function). When a hook calls `retry(updatedOptions)`, got constructs a RetryError to unwind the current response-processing loop and restart the request with the new options. Because the promise wrapper catches it internally and re-issues the request, a caller should never observe it on the rejected promise — seeing it surface means the retry plumbing leaked out of the wrapper, typically because the hook was registered against the stream API (which has no retry loop) instead of the promise API.","triggerScenarios":"Triggered when an afterResponse hook calls its second argument (the retry function) to force a retry, e.g. `hooks: { afterResponse: [(response, retry) => retry({ headers: { authorization: newToken } })] }`. The RetryError is created in as-promise/index.ts:167 and is normally swallowed by the surrounding `try/catch` at line 174 that feeds it back into makeRequest.","commonSituations":"Token-refresh hooks that retry with a new Authorization header, hooks that retry on a 429 after reading Retry-After, or migrating a hook from the promise API to the stream API (got.stream(...)) where the retry callback is unsupported. Also seen when a custom error handler re-throws the internal RetryError instead of letting the wrapper consume it.","solutions":["Use the promise API (got(url), got.get, etc.) rather than got.stream(...) when you rely on afterResponse-driven retries — the RetryError unwinding only works inside asPromise.","Ensure the afterResponse hook always returns either a response object or the result of calling retry(); never throw the RetryError yourself or re-emit it from an 'error' listener.","If you wrap got with a custom handler (extend({ handlers })), make sure your handler awaits the inner promise rather than returning it partially, so the retry loop can complete before your handler resolves."],"exampleFix":"// before — using stream API, retry callback has nowhere to unwind\nawait got.stream('https://api', {\n  hooks: { afterResponse: [(r, retry) => retry({ headers: { authorization: token } })] }\n});\n\n// after — promise API, RetryError is consumed internally\nawait got('https://api', {\n  hooks: { afterResponse: [(r, retry) => retry({ headers: { authorization: token } })] }\n});","handlingStrategy":"validation","validationCode":"// RetryError is internal — never expose it. If you saw it, you used the wrong API.\n// Validate the call site uses the promise API when afterResponse retry is in play:\nfunction assertSupportsRetryHook(client) {\n  if (typeof client.stream === 'function' && client === got.stream) {\n    throw new Error('afterResponse retry callback is only supported on the promise API, not got.stream');\n  }\n}","typeGuard":"import {RetryError} from 'got';\n\nfunction isRetryError(error: unknown): error is RetryError {\n  return error instanceof Error && (error as any).code === 'ERR_RETRYING' && (error as any).name === 'RetryError';\n}","tryCatchPattern":"// If you wrap got with a custom handler, swallow leaked RetryErrors defensively.\ntry {\n  await wrappedClient(url);\n} catch (error) {\n  if (error instanceof Error && (error as any).code === 'ERR_RETRYING') {\n    // internal control-flow leaked out — re-issue via the promise API\n    return got(url, options);\n  }\n  throw error;\n}","preventionTips":["Always use the promise API (got(url)) when afterResponse hooks drive retries; never got.stream(...).","Do not re-throw RetryError from custom error handlers or 'error' event listeners.","Keep custom got.extend handlers transparent — await the inner promise and return its result unchanged."],"tags":["retry","hooks","internal-control-flow","promise-api"],"analyzedSha":"e3924aa1e53a6ca3eb93a43618ce532442a89b40","analyzedAt":"2026-08-03T19:22:24.770Z","schemaVersion":2}