web-dev-qa-db-fra.com

La transmission d'une fonction asynchrone en tant que rappel entraîne la perte de la trace de la pile d'erreurs

J'essaie d'écrire une fonction qui réintroduira une trace de pile lorsqu'un littéral d'objet est lancé. ( Voir cette question connexe ).

Ce que j'ai remarqué, c'est que si un passe une fonction asynchrone en tant que rappel dans une autre fonction appelante asynchrone, si la fonction appelante a un try/catch, et attrape toutes les erreurs et lance une nouvelle erreur, alors la trace de la pile est perdue.

J'en ai essayé plusieurs variantes:

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();

( Code Sandbox )

Sortie: notez mes commentaires dans la trace de la pile

[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

Il me semble que la déclaration d'attente est ce qui fout les choses.

Que se passe t-il ici?

15
dwjohnston

Ce n'est peut-être pas une réponse directe, mais mon équipe et moi construisons une bibliothèque pour gérer les promesses asynchrones/en attente sans avoir besoin de blocs try/catch.

  1. Installez le module

    npm install await-catcher

  2. Importez l'attenteCatcher

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

  3. Utilise le!

Au lieu de faire ceci:

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

Vous pouvez maintenant le faire:

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"

La bibliothèque wait-catcher est simple. Il renvoie un tableau avec deux index.

1) Le premier index contient les résultats/données OR non défini s'il y a une erreur "[ data , undefined]"

2) Le deuxième index contient l'erreur OR indéfini s'il n'y a pas d'erreur "[undefined, error]"


Await-catcher prend également en charge les types en TypeScript. Vous pouvez passer des types à vérifier par rapport à la valeur de retour si vous utilisez TypeScript.

Exemple:

 interface promiseType {
     test: string
 }

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

Nous mettrons à jour notre référentiel GitHub pour inclure la documentation très bientôt. https://github.com/canaanites/await-catcher


MODIFIER:

On dirait que le moteur V8 "perd" la trace de la pile d'erreurs lorsqu'il démarre un nouveau tick. Il renvoie uniquement la pile d'erreurs à partir de ce point. Quelqu'un a répondu à une question similaire ici .

Modifiez votre code en ceci: 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();

Conclusion

Il n'est pas recommandé de lancer une chaîne au lieu d'un objet Error. Il sera plus difficile à déboguer et pourrait entraîner la perte de la trace de la pile d'erreur. Je recommande fortement de lire ceci: Lancer des chaînes au lieu d'erreurs

0
Moe kanan