Передача асинхронной функции в качестве обратного вызова приводит к потере трассировки стека ошибок

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

Я заметил, что если передать асинхронную функцию в качестве обратного вызова в другую асинхронную функцию вызывающего абонента, если вызывающая функция имеет функцию 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.

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

Ответ 1

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

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

Ответ 2

Это не может быть прямым ответом, но мы с моей командой создаем библиотеку для обработки асинхронных/ожидающих обещаний без необходимости использования блоков 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.

Пример:

 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


РЕДАКТИРОВАТЬ:

Похоже, движок 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. Отладка будет сложнее и может привести к потере трассировки стека ошибок. Настоятельно рекомендуем прочитать это: бросать строки вместо ошибок

Ответ 3

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

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

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

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