web-dev-qa-db-fra.com

Traitement des erreurs de rappel JavaScript

Il est courant de valider des arguments et de renvoyer une erreur dans les fonctions.

Cependant, dans la fonction de rappel JavaScript, telle que:

function myFunction(num, callback) {
  if (typeof num !== 'number') return callback(new Error('invalid num'))
  // do something else asynchronously and callback(null, result)
}

J'ai écrit beaucoup de fonctions comme celle-ci, mais je me demande s'il y a quelque chose de potentiellement dangereux. Parce que dans la plupart des cas, l'appelant suppose qu'il s'agit d'une fonction asynchrone et le rappel s'exécutera après le code juste après l'appel de la fonction. Mais si certains arguments ne sont pas valides, la fonction appellera immédiatement le rappel. L'appelant doit donc être prudent face à la situation, c'est-à-dire une séquence d'exécution inattendue.

Je veux entendre des conseils sur cette question. Dois-je bien présumer que tous les rappels asynchrones peuvent être exécutés immédiatement? Ou je devrais utiliser quelque chose comme setTimeout (..., 0) pour convertir une chose synchrone en une chose asynchrone. Ou il y a une meilleure solution que je ne connais pas. Merci.

8
matianfu

Une API doit indiquer qu'elle appelle le rappel soit de manière synchrone (comme Array#sort) ou asynchrone (comme Promise#then), puis obéit toujours à cette garantie documentée. Il ne faut pas mélanger et assortir.

Donc, oui, si vous avez une fonction qui appellera normalement un rappel de manière asynchrone, elle devrait toujours l'appeler de manière asynchrone, quelle que soit la raison pour laquelle l'appel est effectué.

Il y avait un bon exemple dans jQuery: quand jQuery a ajouté les objets "différés" pour la première fois, ils appelaient le callback synchrone si le différé avait déjà été réglé, mais de manière asynchrone, le cas échéant. Cela a été à l'origine de beaucoup de confusion et de bugs, ce qui explique en partie pourquoi les promesses d'ES2015 garantissent que les rappels then et catch seront toujours appelés de manière asynchrone.


Si possible et sans contredire le reste de la base de code, utilisez plutôt Promises plutôt que de simples rappels. Les promesses offrent une sémantique et une possibilité de composition très claires, simples et garanties pour les opérations asynchrones (et l'interopérabilité avec les opérations synchrones).

4
T.J. Crowder

Non, rappeler immédiatement n'est pas dangereux, et retarder intentionnellement l'erreur fait perdre du temps et de la charge. Oui, rappeler immédiatement une erreur peut être très préjudiciable et devrait être évité pour une fonction supposée asynchrone! (Regardez ça, un 180!)

Du point de vue du développeur, il existe de nombreuses bonnes raisons pour lesquelles la configuration ne peut être effectuée qu'après. Par exemple ici :

const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

L'événement listening n'est attaché qu'après l'appel de .listen(8080), car la source de l'événement est renvoyée par l'appel à .listen(). Dans ce cas, l'implémentation de l'événement listening pour appeler de manière synchrone après l'exécution de .listen() échouera.

Voici un autre cas que je voudrais présenter:

var num = '5';

myFunction(num, function callback(err, result) {
  if (err) {
    return myFunction(num, callback);
  }

  // handle result
});

Maintenant, si vous callback avec l'erreur de manière synchrone, ce flux de contrôle produira un stackoverflow. Bien que ce soit la faute du développeur, un stackoverflow est une très mauvaise chose de se produire d'une fonction qui devrait être asynchrone. C’est l’un des avantages de l’utilisation de setImmediate() pour transmettre l’erreur au lieu d’exécuter immédiatement la variable callback.

1
Patrick Roberts

L'appelant de votre fonction asynchrone doit savoir quel sera le résultat de l'appel de la fonction. Il existe une norme pour ce qu'une fonction asynchrone doit renvoyer, Promises.

Si votre fonction retourne un Promise, tout le monde peut facilement comprendre ce qui se passe dans cette fonction. Les promesses ont le rappel de rejet, mais on pourrait se demander si la validation des paramètres doit être gérée en rejetant la promesse ou si une exception doit être levée directement. Quoi qu'il en soit, si l'appelant gère correctement les exceptions à l'aide de la méthode catch, les exceptions levées directement et les rejets seront capturés de la même manière.

function throwingFunction(num) {
  return new Promise(function (resolve, reject) {

    if (typeof num !== 'number') throw new Error('invalid num');
    // do something else asynchronously and callback(null, result)
  };
}

function rejectingFunction(num) {
  return new Promise(function (resolve, reject) {

    if (typeof num !== 'number') reject(new Error('invalid num'));
    // do something else asynchronously and callback(null, result)
  };
}

// Instead of passing the callback, create the promise and provide your callback to the `then` method.

var resultThrowing = throwingFunction(num)
    .then(function (result) { console.log(result); })
    .catch(function (error) { console.log(error); });

var resultRejecting = rejectingFunction(num)
    .then(function (result) { console.log(result); })
    .catch(function (error) { console.log(error); });

Les deux modèles entraîneront la capture de l'erreur et sa consignation.

Si vous utilisez des promesses, l'appelant de votre fonction asynchrone n'aura pas à s'inquiéter de votre implémentation à l'intérieur de la fonction, et vous pourrez soit renvoyer l'erreur directement, soit la refuser à votre guise.

1
Marco Scabbiolo