Асинхронная ошибка JS фиксирует две или три вещи

JavaScript
Асинхронная ошибка JS фиксирует две или три вещи

представлять

Все мы знаем, что try catch не может отлавливать ошибки в асинхронных задачах setTimeout, в чем причина этого? А асинхронный код особенно распространен в js, как можно сравнивать?

Ситуации, которые невозможно поймать

function main() {
  try {
    setTimeout(() => {
      throw new Error('async error')
    }, 1000)
  } catch(e) {
    console.log(e, 'err')
    console.log('continue...')
  }
}

main();

В этом коде функция обратного вызова setTimeout выдает ошибку, которая не будет поймана в catch, что приведет к падению программы с ошибкой.

Так что попробуйте поймать в js не означает, что вы можете сидеть сложа руки и расслабиться после написания одного. Вы должны написать каждую функцию? При каких обстоятельствах можно попытаться поймать ошибку?

асинхронная задача

  • Ошибка в функции обратного вызова макроса не может быть обнаружена

    Каштан выше немного изменен, напишите try catch в основной задаче, а затем вызовите асинхронную задачу, задача выдаст ошибку через одну секунду.

    // 异步任务
    const task = () => {
      setTimeout(() => {
       throw new Error('async error')
     }, 1000)
    }
    // 主任务
    function main() {
      try {
        task();
      } catch(e) {
        console.log(e, 'err')
        console.log('continue...')
      }
    }
    
    

    В этом случае main не может поймать ошибку, связанную с механизмом выполнения браузера. Асинхронная задача добавляется в очередь задач циклом событий и вынимается и помещается в стек (основной процесс js) для выполнения.Когда задача вынимается и выполняется, основной стек уже вышел, то есть контекст изменился, поэтому main не может поймать ошибку задачи.

    Обратные вызовы событий и обратные вызовы запросов относятся к задачам, поэтому рассуждение одинаковое. Обзор Eventloop может видеть этостатья

  • Обратный вызов микрозадачи (обещание)

    // 返回一个 promise 对象
    const promiseFetch = () => 
      new Promise((reslove) => {
      reslove();
    })
    
    function main() {
      try {
        // 回调函数里抛出错误
        promiseFetch().then(() => {
          throw new Error('err')
        })
      } catch(e) {
        console.log(e, 'eeee');
        console.log('continue');
      }
    }
    

    Задача обещания, то есть callback-функция в then, выдает ошибку и не может быть поймана. Поскольку очередь микрозадач очищается между двумя задачами, когда then помещается в стек, основная функция также выталкивается из стека.

Дело не в том, что функция обратного вызова не может попытаться поймать

У многих людей может возникнуть непонимание, потому что большинство ситуаций, которые невозможно отловить, происходят в функции обратного вызова, и они думают, что функция обратного вызова не может быть поймана.

Не совсем так, посмотрите на самый обычный каштан.

// 定义一个 fn,参数是函数。
const fn = (cb: () => void) => {
  cb();
};

function main() {
  try {
    // 传入 callback,fn 执行会调用,并抛出错误。
    fn(() => {
      throw new Error('123');
    })
  } catch(e) {
    console.log('error');
  }
}
main();

Результат конечно достойный. Потому что, когда callback выполняется, он все еще находится в том же цикле событий, что и main, то есть в тике цикла событий. Таким образом, контекст не меняется, и ошибки могут быть обнаружены. Основной причиной по-прежнему является синхронный код, и асинхронные задачи не встречаются.

Перехват исключения для обещания

Конструктор

Сначала посмотрите на два фрагмента кода:

function main1() {
  try {
    new Promise(() => {
      throw new Error('promise1 error')
    })
  } catch(e) {
    console.log(e.message);
  }
}

function main2() {
  try {
    Promise.reject('promise2 error');
  } catch(e) {
    console.log(e.message);
  }
}

Вышеупомянутые два try catch не могут поймать ошибку, потому что ошибка внутри промиса не выскочит наружу, а будет съедена промисом.Его может поймать только promise.catch, поэтому вы должны написать catch при использовании промиса.

Затем давайте посмотрим на два фрагмента кода, в которых используется promise.catch:

// reject
const p1 = new Promise((reslove, reject) => {
  if(1) {
    reject();
  }
});
p1.catch((e) => console.log('p1 error'));
// throw new Error
const p2 = new Promise((reslove, reject) => {
  if(1) {
    throw new Error('p2 error')
  }
});

p2.catch((e) => console.log('p2 error'));

Будь то отклонение или выдача новой ошибки внутри обещания, ее можно поймать обратным вызовом catch.

Здесь нам нужно отличить его от каштанов нашей исходной микрозадачи.Микрозадача обещания относится к обратному вызову then, а вот первый параметр, переданный конструктором промиса, и новый промис выполняется синхронно.

then

Как поймать ошибку после этого.

function main3() {
  Promise.resolve(true).then(() => {
    try {
      throw new Error('then');
    } catch(e) {
      return e;
    }
  }).then(e => console.log(e.message));
}

Вы можете поймать ошибку только внутри функции обратного вызова и вернуть сообщение об ошибке, и ошибка будет передана следующему обратному вызову.

Перехватывайте асинхронные ошибки с помощью промисов


const p3 = () =>  new Promise((reslove, reject) => {
  setTimeout(() => {
    reject('async error');
  })
});

function main3() {
  p3().catch(e => console.log(e));
}
main3();

Оберните асинхронную операцию с помощью Promise, передайте внутреннюю оценку, отклоните ошибку и перехватите ее извне с помощью promise.catch.

Перехват исключения для async/await

Сначала мы симулируем функцию fetchFailure при ошибке запроса, функция выборки обычно возвращает обещание.

Основная функция изменена на асинхронную, а catch перехватывает ошибку, вызванную отклонением fetchFailure. Вы можете получить его.

const fetchFailure = () => new Promise((resolve, reject) => {
  setTimeout(() => {// 模拟请求
    if(1) reject('fetch failure...');
  })
})

async function main () {
  try {
    const res = await fetchFailure();
    console.log(res, 'res');
  } catch(e) {
    console.log(e, 'e.message');
  }
}
main();

Асинхронная функция будет скомпилирована в несколько секций, согласно ключевому слову await и catch, например, основная функция разбита на три секции.

  1. fetchFailure 2. console.log(res) 3. catch

Ход итерации контролируется пошагово, например, «следующий», который должен спуститься один раз, от 1 до 2, асинхронный управляется Promise.then(), вы можете понять это как цепочку обещаний, если вы заинтересованы, вы можете пойти, чтобы исследовать это. Суть в том, что у генератора также есть состояние "throw". Когда состояние Promise отклоняется, оно будет всплывать до тех пор, пока не будет выполнен step('throw'), а затем код в catchconsole.log(e, 'e.message');воплощать в жизнь.

Очевидно, что обработка ошибок в async/await более элегантна, и, конечно же, Promise используется внутри.

Сделайте еще один шаг вперед

Асинхронная функция - мощный инструмент для обработки асинхронных процессов, но она не будет автоматически ловить ошибки. Нам нужно написать try catch самим. Если мы напишем по одной для каждой функции, это довольно хлопотно. В бизнесе много асинхронных функций.

Первое, что приходит на ум, это извлечь try catch и логику после catch.


const handle = async (fn: any) => {
  try {
    return await fn();
  } catch(e) {
    // do sth
    console.log(e, 'e.messagee');
  }
}

async function main () {
    const res = await handle(fetchFailure);
    console.log(res, 'res');
}

Напишите функцию более высокого порядка для переноса fetchFailure, логику повторного использования функции более высокого порядка, например, try catch здесь, а затем выполните входящую функцию-параметр.

Затем добавьте передачу параметров функции обратного вызова и возвращаемое значение для соответствия первой ошибке, что соответствует синтаксису node/go. следующим образом:

const handleTryCatch = (fn: (...args: any[]) => Promise<{}>) => async (...args: any[]) => {
  try {
    return [null, await fn(...args)];
  } catch(e) {
    console.log(e, 'e.messagee');
    return [e];
  }
}

async function main () {
  const [err, res] = await handleTryCatch(fetchFailure)('');
  if(err) {
    console.log(err, 'err');
    return;
  }
  console.log(res, 'res');
}

Но есть еще несколько проблем, одна это логика после catch, которая не поддерживает кастомизацию, и о возвращаемом значении всегда нужно судить, есть ли ошибка, его тоже можно абстрагировать. Поэтому мы можем сделать статью по отлову функций высшего порядка, например добавить какие-то callback-функции для обработки ошибок для поддержки разной логики, и тогда обработку ошибок в проекте можно будет просто разделить на несколько категорий с кодом.

// 1. 三阶函数。第一次传入错误处理的 handle,第二次是传入要修饰的 async 函数,最后返回一个新的 function。

const handleTryCatch = (handle: (e: Error) => void = errorHandle) =>
  (fn: (...args: any[]) => Promise<{}>) => async(...args: any[]) => {
    try {
      return [null, await fn(...args)];
    } catch(e) {
      return [handle(e)];
    }
  }
   
// 2. 定义各种各样的错误类型
//    我们可以把错误信息格式化,成为代码里可以处理的样式,比如包含错误码和错误信息
class DbError extends Error {
  public errmsg: string;
  public errno: number;
  constructor(msg: string, code: number) {
    super(msg);
    this.errmsg = msg || 'db_error_msg';
    this.errno = code || 20010;
  }
}
class ValidatedError extends Error {
  public errmsg: string;
  public errno: number;
  constructor(msg: string, code: number) {
    super(msg);
    this.errmsg = msg || 'validated_error_msg';
    this.errno = code || 20010;
  }
}

// 3. 错误处理的逻辑,这可能只是其中一类。通常错误处理都是按功能需求来划分
//    比如请求失败(200 但是返回值有错误信息),比如 node 中写 db 失败等。
const errorHandle = (e: Error) => {
  // do something
  if(e instanceof ValidatedError || e instanceof DbError) {
    // do sth
    return e;
  }
  return {
    code: 101,
    errmsg: 'unKnown'
  };
}   
const usualHandleTryCatch = handleTryCatch(errorHandle);

// 以上的代码都是多个模块复用的,那实际的业务代码可能只需要这样。
async function main () {
  const [error, res] = await usualHandleTryCatch(fetchFail)(false);
  if(error) {
    // 因为 catch 已经做了拦截,甚至可以加入一些通用逻辑,这里甚至不用判断 if error
    console.log(error, 'error');
    return;
  }
  console.log(res, 'res');
}

После решения проблемы повторного использования некоторой логики ошибок достаточно инкапсулировать ее в разные обработчики ошибок. Но когда используются эти процессоры, поскольку все они являются функциями более высокого порядка, вы можете использовать метод написания декоратора es6.

Однако декораторы можно использовать только в классах и методах классов, поэтому, если они представлены в виде функций, их нельзя использовать. Однако в повседневной разработке, такой как компоненты React или магазин Mobx, все они существуют в виде классов, поэтому существует множество сценариев использования.

Например, измените его на декоратор класса:

const asyncErrorWrapper = (errorHandler: (e: Error) => void = errorHandle) => (target: Function) => {
  const props = Object.getOwnPropertyNames(target.prototype);
  props.forEach((prop) => {
      var value = target.prototype[prop];
      if(Object.prototype.toString.call(value) === '[object AsyncFunction]'){
        target.prototype[prop] = async (...args: any[]) => {
          try{
            return await value.apply(this,args);
          }catch(err){
            return errorHandler(err);
          }
        }
      }
  });
}

@asyncErrorWrapper(errorHandle)
class Store {
  async getList (){
    return Promise.reject('类装饰:失败了');
  }
}

const store = new Store();

async function main() {
  const o = await store.getList();
}
main();

То, как написан этот декоратор класса, это увидетьХуан ЦзыиЯ написал это, спасибо за вдохновение.

Обработка ошибок в koa

Если вы не знакомы с коа, вы можете пропустить его.

Конечно, описанный выше асинхронный метод также можно использовать в koa, но обычно, когда мы используем koa для написания сервера, мы все обрабатываем запросы.Транзакция http удаляет промежуточное программное обеспечение ответа, поэтому обработка ошибок koa помогает использование промежуточного ПО.

Например, мой подход заключается в том, чтобы фиксировать ошибки в первом промежуточном программном обеспечении.Из-за луковой модели первое промежуточное программное обеспечение все равно будет выполняться в конце, и когда промежуточное программное обеспечение выдает ошибку, я ожидаю, что смогу зафиксировать и обработать ее здесь. . . .

// 第一个中间件
const errorCatch = async(ctx, next) => {
  try {
    await next();
  } catch(e) {
    // 在此捕获 error 路由,throw 出的 Error
    console.log(e, e.message, 'error');
    ctx.body = 'error';
  }
}

app.use(errorCatch);

// logger
app.use(async (ctx, next) => {
  console.log(ctx.req.body, 'body');
  await next();
})

// router 的某个中间件
router.get('/error', async (ctx, next) => {
  if(1) {
    throw new Error('错误测试')
  }
  await next();
})


Зачем писать try catch в первом промежуточном программном обеспечении, чтобы перехватывать ошибки, выдаваемые предыдущим промежуточным программным обеспечением? Прежде всего, как мы объясняли ранее в async/await, в asyncawait handle(), внутри функции дескриптораthrow new ErrorилиPromise.reject()Может быть пойман асинхронным уловом. Так что просто нужна следующая функция, чтобы получить ошибку и выдать ее, а затем посмотреть на следующую функцию.

// compose 是传入中间件的数组,最终形成中间件链的,next 控制游标。
 compose(middlewares) {
    return (context) => {
      let index = 0;
      // 为了每个中间件都可以是异步调用,即 `await next()` 这种写法,每个 next 都要返回一个 promise 对象

      function next(index) {
        const func = middlewares[index];
        try {
          // 在此处写 try catch,因为是写到 Promise 构造体中的,所以抛出的错误能被 catch
          return new Promise((resolve, reject) => {
            if (index >= middlewares.length) return reject('next is inexistence');
            resolve(func(context, () => next(index + 1)));
          });
        } catch(err) {
          // 捕获到错误,返回错误
          return Promise.reject(err);
        }
      }
      return next(index);
    }
  }

Следующая функция берет текущее выполнение промежуточного программного обеспечения в соответствии с индексом. Если промежуточная функция является асинхронной функцией, она преобразуется в выполнение генератора, а внутренняя последовательность асинхронного кода контролируется сама по себе, и мы знаем, что ошибка асинхронной функции может быть перехвачена с помощью try catch, поэтому добавьте try catch в следующая функция.Ошибка функции промежуточного программного обеспечения может быть вызвана возвратом. Итак, мы можем захватить первое промежуточное ПО. Подробный код можно увидеть нижесокращенно коа

Затем koa также предоставляет ctx.throw и global app.on для отлова ошибок. Если вы не пишете промежуточное программное обеспечение для обработки ошибок, вы можете использовать ctx.throw для возврата к внешнему интерфейсу, чтобы не допустить ошибки кода. Но throw new Error также выгоден, потому что в логике кода определенного промежуточного программного обеспечения, когда мы не хотим, чтобы промежуточное программное обеспечение выполнялось, возвращаем его непосредственно во внешний интерфейс, выбрасываем ошибку напрямую и позволяем общему промежуточному программному обеспечению обрабатывать ее, в любом случае это неверная информация.

// 定义不同的错误类型,在此可以捕获,并处理。
const errorCatch = async(ctx, next) => {
  try {
    await next();
 } catch (err) {
    const { errmsg, errno, status = 500, redirect } = err;
    
    if (err instanceof ValidatedError || err instanceof DbError || err instanceof AuthError || err instanceof RequestError) {
      ctx.status = 200;
      ctx.body = {
        errmsg,
        errno,
      };
      return;
    }
    ctx.status = status;
    if (status === 302 && redirect) {
      console.log(redirect);
      ctx.redirect(redirect);
    }
    if (status === 500) {
      ctx.body = {
        errmsg: err.message,
        errno: 90001,
      };
      ctx.app.emit('error', err, ctx);
    }
  }
}

app.use(errorCatch);

// logger
app.use(async (ctx, next) => {
  console.log(ctx.req.body, 'body');
  await next();
})

// 通过 ctx.throw
app.use(async (ctx, next) => {
  //will NOT log the error and will return `Error Message` as the response body with status 400
  ctx.throw(400,'Error Message');
}); 

// router 的某个中间件
router.get('/error', async (ctx, next) => {
  if(1) {
    throw new Error('错误测试')
  }
  await next();
})

// 最后的兜底
app.on('error', (err, ctx) => {
  /* centralized error handling:
   *   console.log error
   *   write error to log file
   *   save error and request information to database if ctx.request match condition
   *   ...
  */
});

наконец

Код этой статьи хранится вэто

В общем, удобнее совмещать асинхронность с промисом, чтобы иметь дело с асинхронными ошибками js. Кроме того, у зрелых фреймворков (react, koa) есть хорошие способы обработки ошибок, попробуйте посмотреть, как с этим справится официальный.

Это лишь часть моего понимания обработки асинхронных ошибок в js. Тем не менее, есть много мест, где исключения должны быть перехвачены во внешнем интерфейсе, например, ошибки кода внешнего интерфейса, междоменные ошибки Cors, ошибки iframe и даже ошибки реакции и vue, с которыми нам нужно иметь дело, а также мониторинг исключений и отчетность, чтобы помочь нам решить их своевременно проблем и аналитической стабильности. Мы берем самые разные решения и применяем их в наших проектах, чтобы нам не пришлось снова беспокоиться о зависании страницы или багах, чтобы мы могли отправиться в отпуск и отдохнуть в целости и сохранности😆

Наконец, адрес блога:GitHub.com/Сунь Юнцзянь…