Передача асинхронной функции в качестве обратного вызова приводит к потере трассировки стека ошибок - PullRequest
14 голосов
/ 18 июня 2019

Я пытаюсь написать функцию, которая будет повторно вводить трассировку стека при генерировании литерала объекта. ( См. Этот связанный вопрос ).

Что я заметил, так это то, что если передать асинхронную функцию в качестве обратного вызова в другую асинхронную функцию вызывающего абонента, если вызывающая функция имеет функцию try / catch, перехватывает любые ошибки и выдает новую ошибку, то трассировка стека теряется

Я пробовал несколько вариантов этого:

function alpha() {
  throw Error("I am an error!");
}

function alphaObectLiberal() {
  throw "I am an object literal!";  //Ordinarily this will cause the stack trace to be lost. 
}

function syncFunctionCaller(fn) {
  return fn();
}

function syncFunctionCaller2(fn) { //This wrapper wraps it in a proper error and subsequently preserves the stack trace. 
  try {
    return fn();
  } catch (err) {
    throw new Error(err); //Stack trace is preserved when it is synchronous. 
  }
}


async function asyncAlpha() {
  throw Error("I am also an error!"); //Stack trace is preseved if a proper error is thown from callback
}

async function asyncAlphaObjectLiteral() {
  throw "I am an object literal!"; //I want to catch this, and convert it to a proper Error object. 
}

async function asyncFunctionCaller(fn) {
  return await fn();
}

async function asyncFunctionCaller2(fn) {
  try {
    await fn();
  } catch (err) {
    throw new Error(err);
  }
}

async function asyncFunctionCaller3(fn) {
  try {
    await fn();
  } catch (err) {
    throw new Error("I'm an error thrown from the function caller!");
  }
}

async function asyncFunctionCaller4(fn) {
  throw new Error("No try catch here!");
}

async function everything() {
  try {
    syncFunctionCaller(alpha);
  } catch (err) {
    console.log(err);
  }


  try {
    syncFunctionCaller2(alphaObectLiberal);
  } catch (err) {
    console.log(err);
  }

  try {
    await asyncFunctionCaller(asyncAlpha);
  } catch (err) {
    console.log(err);
  }

  try {
    await asyncFunctionCaller2(asyncAlphaObjectLiteral);
  } catch (err) {
    console.log(err); //We've lost the `everthing` line number from the stack trace
  }

  try {
    await asyncFunctionCaller3(asyncAlphaObjectLiteral);
  } catch (err) {
    console.log(err); //We've lost the `everthing` line number from the stack trace
  }

  try {
    await asyncFunctionCaller4(asyncAlphaObjectLiteral);
  } catch (err) {
    console.log(err); //This one is fine
  }
}

everything();

( Код Песочница )

Вывод: обратите внимание на мои комментарии в трассировке стека

[nodemon] starting `node src/index.js localhost 8080`
Error: I am an error!
    at alpha (/sandbox/src/index.js:2:9)
    at syncFunctionCaller (/sandbox/src/index.js:6:10)
    at everything (/sandbox/src/index.js:43:5) 
    //We can see what function caused this error
    at Object.<anonymous> (/sandbox/src/index.js:73:1)
    at Module._compile (internal/modules/cjs/loader.js:776:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
Error: I am an object literal!
    at syncFunctionCaller2 (/sandbox/src/index.js:17:11)
    at everything (/sandbox/src/index.js:65:5)
    //In a synchronous wrapper, the stack trace is preserved
    at Object.<anonymous> (/sandbox/src/index.js:95:1)
    at Module._compile (internal/modules/cjs/loader.js:776:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
    at startup (internal/bootstrap/node.js:283:19)
Error: I am also an error!
    at asyncAlpha (/sandbox/src/index.js:10:9)
    at asyncFunctionCaller (/sandbox/src/index.js:18:16)
    at everything (/sandbox/src/index.js:49:11) 
    //We can see what function caused this error
    at Object.<anonymous> (/sandbox/src/index.js:73:1)
    at Module._compile (internal/modules/cjs/loader.js:776:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
Error: I am an object literal!
    at asyncFunctionCaller2 (/sandbox/src/index.js:25:11) 
   //We've lost the stacktrace in `everything`
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:832:11)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
Error: I'm an error thrown from the function caller!
    at asyncFunctionCaller3 (/sandbox/src/index.js:33:11)
    //We've lost the stacktrace in `everything`
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:832:11)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
Error: No try catch here!
    at asyncFunctionCaller4 (/sandbox/src/index.js:38:9)
    at everything (/sandbox/src/index.js:67:11)
    //We can see what function caused this error
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:832:11)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
[nodemon] clean exit - waiting for changes before restart

Мне кажется, что это все испортит утверждение await.

Что здесь происходит?

Ответы [ 3 ]

6 голосов
/ 21 июня 2019

Отсутствие трассировки стека не имеет ничего общего с обещаниями. Напишите тот же код, который имеет функции, вызывающие друг друга синхронно, и вы увидите точно такое же поведение, т. Е. Потеря данных полной трассировки стека при повторной выдаче new Error. Это только Error объект, который предлагает доступ к стеку. Он, в свою очередь, поддерживается собственным кодом (например, это из механизма V8 ), отвечающим за захват трассировки стека пересеченных кадров стека. Хуже того, каждый раз, когда вы создаете объект Error, он захватывает стек из этой точки через стековые рамки (по крайней мере, это можно наблюдать в браузере, реализация nodejs может отличаться). Таким образом, если вы перехватываете и перезагружаете другой объект Error, его трассировка стека видна поверх пузырькового исключения. Отсутствие цепочки исключений для Error (без возможности обернуть новое исключение вокруг пойманного) затрудняет заполнение этих пробелов. Более интересным является то, что Спецификация ECMA-262, глава 19.5 , вообще не вводит свойство Error.prototype.stack, в MDN, в свою очередь, вы обнаружите, что свойство стека является нестандартным расширением движка JS.

РЕДАКТИРОВАТЬ: Относительно отсутствующей функции «все» в стеке, это побочный эффект от того, как движок переводит «асинхронный / ожидающий» в вызовы микрозадач и кто действительно вызывает конкретные обратные вызовы. См. Объяснение команды разработчиков V8 , а также их трассировки асинхронных стеков нулевой стоимости , в которых содержатся подробные сведения. NodeJS, начиная с версии 12.x , будет включать более чистых следов стека, доступных с опцией --async-stack-traces, предлагаемой двигателем V8.

0 голосов
/ 23 июня 2019

Возможно, это не прямой ответ, но мы с моей командой создаем библиотеку для обработки асинхронных / ожидающих обещаний без необходимости использования блоков try / catch.

  1. Установить модуль

    npm install await-catcher

  2. Импорт awaitCatcher

    const { awaitCatcher } = require("await-catcher")

  3. Используй это!

Вместо этого:

async function asyncFunctionCaller2(fn) {
  try {
    await fn();
  } catch (err) {
    throw new Error(err);
  }
}

Теперь вы можете сделать это:

async function asyncFunctionCaller2(fn) {
  let [ data, err ] = await awaitCatcher(fn);

  // Now you can do whatever you want with data or error
  if ( err ) throw err;
  if ( data ) return data;
}
  // Note:
  // You can name the variables whatever you want. 
  // They don't have to be "data" or "err"

Библиотека await-catcher проста. Возвращает массив с двумя индексами.

1) Первый индекс содержит результаты / данные ИЛИ неопределенный в случае ошибки "[ data , undefined]"

2) Второй индекс содержит ошибку ИЛИ неопределенную, если ошибки нет "[undefined, error]"


Await-catcher также поддерживает типы в TypeScript. Вы можете передавать типы для проверки по возвращаемому значению, если используете TypeScript.

* +1039 * Пример:
 interface promiseType {
     test: string
 }

 (async () => {
     let p = Promise.resolve({test: "hi mom"})
     let [ data , error ] = await awaitCatcher<promiseType>(p);
     console.log(data, error);
 })()

Мы обновим наше репозиторий GitHub, чтобы очень скоро включить документацию. https://github.com/canaanites/await-catcher


EDIT:

Похоже, что движок V8 "теряет" трассировку стека ошибок при запуске нового тика. Он возвращает только стек ошибок с этой точки. Кто-то ответил на аналогичный вопрос здесь .

Измените свой код на это: https://codesandbox.io/embed/empty-wave-k3tdj

const { awaitCatcher } = require("await-catcher");

async function asyncAlphaObjectLiteral() {
  throw Error("I am an object literal!"); // 1) You need to create an Error object here

  // ~~~~> try throwing just a string and see the difference
}

async function asyncFunctionCaller2(fn) {
  try {
    await fn();
  } catch (err) {
    throw err; // 2) Don't create a new error, just throw the error.
  }
}

/**
 * Or you can just do this...
 * the "awaitCatcher" will catch the errors :)
 *
 * async function asyncFunctionCaller2(fn) {
 *  await fn();
 * }
 */

async function everything() {
  /**
   * notice we don't need try/catch here either!
   */

  let [data, error] = await awaitCatcher(
    asyncFunctionCaller2(asyncAlphaObjectLiteral)
  );
  console.log(error); // 3) Now you have the full error stack trace
}

everything();

Заключение

Не рекомендуется бросать строку вместо объекта Error. Отладка будет сложнее и может привести к потере трассировки стека ошибок. Настоятельно рекомендуем прочитать это: Бросать строки вместо ошибок

0 голосов
/ 21 июня 2019

РЕДАКТИРОВАТЬ: этот ответ кажется абсолютно неправильным , см. Ответ @andy, который точно описывает, что здесь происходит.

Я думаю, что контекст не совсем потерян - онникогда не был тамВы используете async / await, и ваш код эффективно разбивается на «куски», которые выполняются несколько нелинейным способом - асинхронно.Это означает, что в определенных точках интерпретатор покидает основной поток, выполняет 'тик' (таким образом, вы видите process._tickCallback в stacktrace) и выполняет следующий "кусок".

Почему это происходит?Поскольку async / await является синтаксическим сахаром для Promise, который представляет собой красиво упакованные обратные вызовы, управляемые внешними событиями (я считаю, что в данном конкретном случае это таймер).

Что вы можете сделать с этим?Может быть, не могу сказать наверняка, как никогда этого не делал.Но я думаю, что следующее - хорошее начало: https://github.com/nodejs/node/issues/11865

...