-1

I've written a retry function. But when fn returns error, the .catch cannot output the error and execute attempt().

I call it by using

const res = await retry(fetch, url) as Response;

The console in browser outputs 429 (Too Many Requests. Please try later) but this is not from my code.

export function retry(fn: Function, params: any, times = 1e9 + 7) {
  return new Promise((resolve, reject) => {
    const attempt = () => {
      fn(params).then(resolve).catch((err: Error) => {
        console.log(err);
        times-- > 0 ? attempt() : reject("fail");
      })
    }

    attempt();
  })
}
Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
SNORLAX
  • 135
  • 8
  • So show the full error stack trace. Don't redact or summarize, just copy and paste the actual full error and stack trace from your dev tools, so that others can see what you're talking about. You're passing `fetch` and the URL that should fetch, so what happens if you just directly call `fetch(url)` instead of wrapping it in your retry code? Does that work? (And if it does, put that in your post. And if it doesn't, _also_ [put that in your post](/help/how-to-ask) =) – Mike 'Pomax' Kamermans Jun 12 '23 at 00:48
  • 1
    Avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! – Bergi Jun 12 '23 at 01:15
  • 1
    Now I know the reason, fetch api does not throw error of 4xx or 5xx. – SNORLAX Jun 12 '23 at 14:04
  • @Mike'Pomax'Kamermans - Why do you mark a dup that is not a dup of the question at all. The questions are simply not the same. This question doesn't even mention `fetch()` as the source until 12 hours later in a comment and that's still not stated in the question itself. The answer below is the real answer. `fn()`, no matter what it is (and per the question, it could be many things as its a generic callback), is not rejecting. That's the real answer here. For all we know `fn()` could be many things, some of which are not `fetch()`. – jfriend00 Jun 13 '23 at 02:57
  • If this is not browser code, this is why I don't use `fetch()` myself as I prefer to use a library that, by default makes 4xx and 5xx errors into rejections. I also dislike that it takes two asynchronous operations to read the content, one to get headers and one to read the content. I use `got()` myself, but `axios()` or `bent()` also work. – jfriend00 Jun 13 '23 at 03:01
  • @jfriend00 The question is asking why `retry(fetch)` is not catching anythig despite a 429 error in the console, believing that "*fn returns error*", and [this](https://stackoverflow.com/a/38236296/1048572) is our canonical answer for that. The OP even acknowledged that the duplicate solves his problem. – Bergi Jun 13 '23 at 03:07
  • @Bergi - But, that's a canonical for a question about `fetch()` not for a generic callback passed into the function in the question. This question is not written just for `fetch()` - it doesn't even mention `fetch()`. This place is way overboard on marking things dup that benefit from more specific answers to the actual question. I think more specific answers are better than making lots of duplicates. If you wanted, you could make almost every single question here a dup of something as there are very few actual original causes of problems. But, that wouldn't help nearly as many people. – jfriend00 Jun 13 '23 at 03:14
  • 1
    The question is based on expecting fetch, specifically, to error on a 429 code. If anything, this was an XY problem: the promise was not the actual problem. That doesn't invalidate your answer, but the _actual_ problem the user had was a duplicate of why fetch doesn't throw on 4xx. – Mike 'Pomax' Kamermans Jun 13 '23 at 03:35

1 Answers1

2

If you're getting an error in the logs, but it's not going into your .catch() handler, then the only explanation for that is that whatever fn() you're calling is not returning a promise that gets rejected when that error happens. So, further debugging on that specific issue would need to take place in the specific fn function that causes this problem.

The specific 429 error is a rate limiting type error where you're likely making too many requests too quickly. Given your code, this could easily happen if the request fails for some persistent reason, so banging on it over and over with retry requests just continues to fail the same way until eventually the target host says you're making too many requests too quickly.

Nearly all production-quality retry systems that are going to do many consecutive retries will implement a timer backoff (each request waiting longer than the previous one before trying) so you don't just hammer retry requests one after another rapid fire. Instead, you provide the host some time to recover or perhaps remedy whatever the issue is that's causing the error and hopefully also avoid rate limiting restrictions. Having any client do rapid fire retries with no backoff is a recipe for what's known as an avalanche failure on the host. An avalanche failure is where one small problem on the server causes lots of clients to go into massive retry loops just hammering the host so hard that it can't handle all the incoming requests and can't recover from whatever the original problem was. An appropriate backoff in the client prevents this. Hosts may also try to protect themselves with rate limiting (which you are perhaps running into). There is also no benefit to you repeating the same request over and over faster than the target host's rate limit.

Then, lastly, you can clean up your code by removing the new Promise() as you can just chain promises from successive calls and return that promise chain. Here's an implementation with that cleanup and a simple backoff algorithm.

function delay(t) {
    return new Promise(resolve => setTimeout(resolve, t));
}

const kMinRetryTime = 100;
const kPerRetryAdditionalTime = 500;

function calcBackoff(retries) {
    return Math.max(kMinRetryTime, (retries - 1) * kPerRetryAdditionalTime);
}

export function retry(fn: Function, params: any, times = 1e9 + 7) {
    let retries = 0;

    function attempt() {
        return fn(params).catch((err: Error) => {
            ++retries;
            console.log(err);
            if (retries <= times) {
                // still more retries left
                return delay(calcBackoff(retries)).then(attempt);
            } else {
                // bail with error
                throw err;
            }
        })
    }

    return attempt();
}
jfriend00
  • 683,504
  • 96
  • 985
  • 979