Handle retry logic in the front end gracefully

1. Less elegant logic

"Retry" is a very common scenario in front-end development, but because Promiseof the asynchronous callback feature of , we can easily fall into "callback hell" when dealing with retry logic, such as this recursion Promisecombined with retry Test function:

// 工具函数,用于延迟一段时间
const sleep = (second: number = 1000) => {
    
    
  return new Promise((resolve) => {
    
    
    setTimeout(resolve, second);
  });
};

// 工具函数,用于递归重试函数
const retry = <T>(
  promise: () => Promise<T>,
  resolve: (value: unknown) => void,
  reject: (reason?: any) => void,
  retryTimes: number,
  retryMaxTimes: number
) => {
    
    
  sleep().then(() => {
    
    
    promise()
      .then((res: any) => {
    
    
        resolve(res);
      })
      .catch((err: any) => {
    
    
        if (retryTimes >= retryMaxTimes) {
    
    
          reject(err);
          return;
        }
        retry(promise, resolve, reject, retryTimes + 1, retryMaxTimes);
      });
  });
};

// 重试函数
export const retryRequest = <T>(
  promise: () => Promise<T>,
  retryMaxTimes: number
) => {
    
    
  return new Promise((resolve, reject) => {
    
    
    let count = 1;
    promise()
      .then((res) => {
    
    
        resolve(res);
      })
      .catch((err) => {
    
    
        if (count >= retryMaxTimes) {
    
    
          reject(err);
          return;
        }
        retry(promise, resolve, reject, count + 1, retryMaxTimes);
      });
  });
};

With asyncand await, the situation is not much better, we still have to use recursion, and it is not very intuitive to wrap a layer of "try - catch" outside awaitthe function :

// 工具函数,用于延迟一段时间
const sleep = (second: number = 1000) => {
    
    
  return new Promise((resolve) => {
    
    
    setTimeout(resolve, second);
  });
};

// 工具函数,用于递归重试函数
const retry = async <T>(
  promise: () => Promise<T>,
  retryTimes: number,
  retryMaxTimes: number
) => {
    
    
  await sleep();

  try {
    
    
    let result = await promise();
    return result;
  } catch (err) {
    
    
    if (retryTimes >= retryMaxTimes) {
    
    
      throw err;
    }
    retry(promise(), retryTimes + 1, retryMaxTimes);
  }
};

// 重试函数
export const retryRequest = async <T>(
  promise: () => Promise<T>,
  retryMaxTimes: number
) => {
    
    
  let count = 1;
  try {
    
    
    let result = await promise();
    return result;
  } catch (err) {
    
    
    if (count >= retryMaxTimes) {
    
    
      throw err;
    }
    retry(promise(), count + 1, retryMaxTimes);
  }
};

2. Elegant logic

So how to write retry logic elegantly? Combined with the less elegant logic in the first part, we can know that if we want to improve readability, we have to do the following three things:

1. Eliminate callbacks
2. Eliminate try-catch exception handling logic
3. Eliminate recursive logic

2-1. Callback and exception handling

In order to eliminate the callback function, we still choose asyncthe and awaitsyntax, and for exception handling, we encapsulate a function to level the exception handling logic, as follows:

const awaitErrorWrap = async <T, U = any>(
  promise: Promise<T>
): Promise<[U | null, T | null]> => {
    
    
  try {
    
    
    const data = await promise;
    return [null, data];
  } catch (err: any) {
    
    
    return [err, null];
  }
};

In this way, we can get the exception directly without wrapping try-catch like gothe language , as follows:

const [err, data] = await awaitErrorWrap(promise);

2-2. Change the recursive function into a loop

We change the original recursive function into a forloop , so that the logic is clearer:

const retryRequest = async <T>(
  promise: () => Promise<T>,
  retryTimes: number = 3
) => {
    
    
  let output: [any, T | null] = [null, null];

  for (let a = 0; a < retryTimes; a++) {
    
    
    output = await awaitErrorWrap(promise());

    if (output[1]) {
    
    
      break;
    }
  }

  return output;
};

2-3. Elegant retry logic

Then we combine the above codes to get the following very concise and elegant retry logic:

// 工具函数,用于延迟一段时间
const sleep = (time: number) => {
    
    
  return new Promise((resolve) => {
    
    
    setTimeout(resolve, time);
  });
};

// 工具函数,用于包裹 try - catch 逻辑
const awaitErrorWrap = async <T, U = any>(
  promise: Promise<T>
): Promise<[U | null, T | null]> => {
    
    
  try {
    
    
    const data = await promise;
    return [null, data];
  } catch (err: any) {
    
    
    return [err, null];
  }
};

// 重试函数
export const retryRequest = async <T>(
  promise: () => Promise<T>,
  retryTimes: number = 3,
  retryInterval: number = 500
) => {
    
    
  let output: [any, T | null] = [null, null];

  for (let a = 0; a < retryTimes; a++) {
    
    
    output = await awaitErrorWrap(promise());

    if (output[1]) {
    
    
      break;
    }

    console.log(`retry ${
      
      a + 1} times, error: ${
      
      output[0]}`);
    await sleep(retryInterval);
  }

  return output;
};

We can use it like this:

import {
    
     retryRequest } from "xxxx";
import axios from "axios";

const request = (url: string) => {
    
    
  return axios.get(url);
};

const [err, data] = await retryRequest(request("https://request_url"), 3, 500);

2-4. Going further? Write a decorator

Of course, after writing here, some friends will definitely say: "It's not elegant enough to Promisewrap !"

No problem, we can go one step further and write typescripta decorator for :

export const retryDecorator = (
  retryTimes: number = 3,
  retryInterval: number = 500
): MethodDecorator => {
    
    
  return (_: any, __: string | symbol, descriptor: any) => {
    
    
    const fn = descriptor.value;
    descriptor.value = async function (...args: any[]) {
    
    
      // 这里的 retryRequest 就是刚才的重试函数
      return retryRequest(fn.apply(this, args), retryTimes, retryInterval);
    };
  };
};

So we can use this decorator directly like this:

import {
    
     retryRequest } from "xxxx";
import axios from "axios";

class RequestClass {
    
    
  @retryDecorator(3, 500)
  static async getUrl(url: string) {
    
    
    return axios.get(url);
  }
}

const [err, data] = await RequestClass.getUrl("https://request_url");

2-5. How about further optimization?

Some people may think that throwing the exception directly as a variable is not intuitive, no problem, let's change to a version that can use try catch normally:

// 重试函数
export const retryRequest = async <T>(
  promise: () => Promise<T>,
  retryTimes: number = 3,
  retryInterval: number = 500
) => {
    
    
  let output: [any, T | null] = [null, null];

  for (let a = 0; a < retryTimes; a++) {
    
    
    output = await awaitErrorWrap(promise());

    if (output[1]) {
    
    
      break;
    }

    console.log(`retry ${
      
      a + 1} times, error: ${
      
      output[0]}`);
    await sleep(retryInterval);
  }

  if (output[0]) {
    
    
    throw output[0];
  }

  return output[1];
};

We can use it normally as a try catch:

import {
    
     retryRequest } from "xxxx";
import axios from "axios";

const request = (url: string) => {
    
    
  return axios.get(url);
};

try {
    
    
  const res = await retryRequest(request("https://request_url"), 3, 500);
} catch (err) {
    
    
  console.error(res);
}

Guess you like

Origin blog.csdn.net/u011748319/article/details/123828513