Article original : How to Simplify Asynchronous JavaScript using the Result-Error Pattern
Par Ken Snyder
Au cours des 18 dernières années de programmation, j'ai dû gérer le comportement asynchrone dans pratiquement tous les projets.
Depuis l'adoption de async-await en JavaScript, nous avons appris que async-await rend beaucoup de code plus agréable et plus facile à comprendre.
Récemment, j'ai remarqué que lorsque je travaille avec une ressource qui doit se connecter et se déconnecter de manière asynchrone, j'en viens à écrire du code comme ceci :
// PAS MON MOTIF PRÉFÉRÉ
router.get('/users/:id', async (req, res) => {
const client = new Client();
let user;
try {
await client.connect();
user = await client.find('users').where('id', req.path.id);
} catch(error) {
res.status(500);
user = { error };
} finally {
await client.close();
}
res.json(user);
});
Cela devient verbeux car nous devons utiliser try/catch pour gérer les erreurs.
Des exemples de telles ressources incluent les bases de données, ElasticSearch, les lignes de commande et ssh.
Dans ces cas d'utilisation, j'ai adopté un motif de code que j'appelle le motif Résultat-Erreur.
Considérez la réécriture du code ci-dessus comme ceci :
// JE PRÉFÈRE CE MOTIF
router.get('/users/:id', async (req, res) => {
const { result: user, error } = await withDbClient(client => {
return client.find('users').where('id', req.path.id);
});
if (error) {
res.status(500);
}
res.json({ user, error });
});
Remarquez quelques points :
- Le client de base de données est créé pour nous et notre callback peut simplement l'utiliser.
- Au lieu de capturer les erreurs dans un bloc try-catch, nous nous reposons sur
withDbClientpour retourner les erreurs. - Le résultat est toujours appelé
resultcar notre callback peut retourner n'importe quel type de données. - Nous n'avons pas à fermer la ressource.
Alors, que fait withDbClient ?
- Il gère la création de la ressource, la connexion et la fermeture.
- Il gère try, catch et finally.
- Il garantit qu'il n'y aura pas d'exceptions non capturées lancées depuis
withDbClient. - Il garantit que toute exception lancée dans le gestionnaire est également capturée à l'intérieur de
withDbClient. - Il garantit que
{ result, error }sera toujours retourné.
Voici un exemple d'implémentation :
// EXEMPLE D'IMPLÉMENTATION
async function withDbClient(handler) {
const client = new DbClient();
let result = null;
let error = null;
try {
await client.connect();
result = await handler(client);
} catch (e) {
error = e;
} finally {
await client.close();
}
return { result, error };
}
Un pas plus loin
Photo par Tom Fisk de Pexels
Et pour une ressource qui n'a pas besoin d'être fermée ? Eh bien, le motif Résultat-Erreur peut toujours être agréable !
Considérez l'utilisation suivante de fetch :
// C'EST COURT ET AGRÉABLE
const { data, error, response } = await fetchJson('/users/123');
Son implémentation pourrait être la suivante :
// EXEMPLE D'IMPLÉMENTATION
async function fetchJson(...args) {
let data = null;
let error = null;
let response = null;
try {
const response = await fetch(...args);
if (response.ok) {
try {
data = await response.json();
} catch (e) {
// pas du json
}
} else {
// notez que statusText est toujours "" dans HTTP2
error = `${response.status} ${response.statusText}`;
}
} catch(e) {
error = e;
}
return { data, error, response };
}
Utilisation de haut niveau
Photo par 16018388 de Pixabay
Nous n'avons pas à nous arrêter à une utilisation de bas niveau. Qu'en est-il des autres fonctions qui peuvent se terminer par un résultat ou une erreur ?
Récemment, j'ai écrit une application avec beaucoup d'interactions ElasticSearch. J'ai décidé d'utiliser également le motif Résultat-Erreur sur des fonctions de haut niveau.
Par exemple, la recherche de posts produit un tableau de documents ElasticSearch et retourne le résultat et l'erreur comme ceci :
const { result, error, details } = await findPosts(query);
Si vous avez travaillé avec ElasticSearch, vous savez que les réponses sont verbeuses et que les données sont imbriquées à plusieurs niveaux dans la réponse. Ici, result est un objet contenant :
records– Un tableau de documentstotal– Le nombre total de documents si une limite n'a pas été appliquéeaggregations– Informations de recherche facettée d'ElasticSearch
Comme vous pouvez le deviner, error peut être un message d'erreur et details est la réponse complète d'ElasticSearch au cas où vous auriez besoin de choses comme les métadonnées d'erreur, les surlignages ou le temps de requête.
Mon implémentation pour interroger ElasticSearch avec un objet de requête ressemble à ceci :
// Récupérer depuis le nom d'index donné avec la requête donnée
async function query(index, query) {
// Notre motif Résultat-Erreur au niveau bas
const { result, error } = await withEsClient(client => {
return client.search({
index,
body: query.getQuery(),
});
});
// Retourner un objet similaire également avec résultat-erreur
return {
result: formatRecords(result),
error,
details: result || error?.meta,
};
}
// Extraire les enregistrements des réponses
function formatRecords(result) {
// Remarquez à quel point ElasticSearch enterre les résultats ?
if (result?.body?.hits?.hits) {
const records = [];
for (const hit of result.body.hits.hits) {
records.push(hit._source);
}
return {
records,
total: result.body.hits.total?.value || 0,
aggregations: result.aggregations,
};
} else {
return { records: [], total: null, aggregations: null };
}
}
Et ensuite, la fonction findPosts devient quelque chose de simple comme ceci :
function findPosts(query) {
return query('posts', query);
}
Résumé
Voici les aspects clés d'une fonction qui implémente le motif Résultat-Erreur :
- Ne jamais lancer d'exceptions.
- Toujours retourner un objet avec les résultats et l'erreur, où l'un peut être null.
- Cacher toute création ou nettoyage de ressource asynchrone.
Et voici les avantages correspondants de l'appel de fonctions qui implémentent le motif Résultat-Erreur :
- Vous n'avez pas besoin d'utiliser des blocs try-catch.
- La gestion des cas d'erreur est aussi simple que
if (error). - Vous n'avez pas besoin de vous soucier des opérations de configuration ou de nettoyage.
Ne me croyez pas sur parole, essayez par vous-même !