| theme | ./theme | ||
|---|---|---|---|
| titleTemplate | L’asynchrone en JS sans le cringe | ||
| background | /jeshoots-com--2vD8lIhdnw-unsplash.jpg | ||
| download | true | ||
| exportFilename | asynchrone-en-js-sans-le-cringe | ||
| class | text-center | ||
| highlighter | shiki | ||
| lineNumbers | true | ||
| favicon | https://delicious-insights.com/apple-touch-icon.png | ||
| info | ## L’asynchrone en JS sans le cringe Une présentation de Christophe Porteneuve Envie de plus ? Notre [chaîne YouTube](https://www.youtube.com/c/DeliciousInsights) et nos [super formations](https://delicious-insights.com/fr/formations/) sont pour toi ! | ||
| drawings |
|
||
| css | unocss |
Une présentation de Christophe Porteneuve
const christophe = {
family: { wife: 'Élodie', sons: ['Maxence', 'Elliott'] },
city: 'Paris, FR',
company: 'Delicious Insights',
trainings: ['TypeScript', 'React PWA', 'Node.js', 'ES Total'],
jsSince: 1995,
claimsToFame: [
'Prototype.js',
'script.aculo.us',
'Bien Développer pour le Web 2.0',
'NodeSchool Paris',
'Paris Web',
'dotJS'
]
}export async function getAllRoles(req, res) {
res.send({ data: ROLES })
}
export async function getAllRolesWithAbilities(req, res) {
const data = computeCombinedAbilitiesByRole(Object.keys(ROLES))
res.send({ data })
}
async function create(createData) {
return GeneralParameter.create(createData)
}export function getAllRoles(req, res) {
res.send({ data: ROLES })
}
export function getAllRolesWithAbilities(req, res) {
const data = computeCombinedAbilitiesByRole(Object.keys(ROLES))
res.send({ data })
}
function create(createData) {
return GeneralParameter.create(createData)
}const mailSequence = mails.map(async (mail) => await sendMail(mail))const mailSequence = mails.map(async (mail) => await sendMail(mail))const mailSequence = mails.map((mail) => sendMail(mail))(Éventuellement, si tu peux garantir que sendMail n’utilise que son premier argument, et ne sera donc pas gêné par des arguments supplémentaires :)
const mailSequence = mails.map(sendMail)const mailSequence = mails.map(async (mail) => await sendMail(mail))Parallélisé (court-circuit sur 1ère erreur temporelle) :
const mailSequence = await Promise.all(mails.map((mail) => sendMail(mail)))Séquencé (court-circuit sur première erreur itérative) :
const mailSequence = []
for (const mail of mails) {
mailSequence.push(await sendMail(mail))
}async function getUserById(id) {
const user = await User.findByPk(…)
return user
}
async function upsertSetting(formObject) {
// …
return noRecord ? await create(fields) : await update(fields)
}
async function renewToken({ commit }) {
const { token, refreshToken } = await renewToken()
const setToken = await commit('setToken', { token, refreshToken })
return setToken
}function getUserById(id) {
return User.findByPk(…)
}
function upsertSetting(formObject) {
// …
return noRecord ? create(fields) : update(fields)
}
async function renewToken({ commit }) {
const { token, refreshToken } = await renewToken()
return commit('setToken', { token, refreshToken })
}Si on transforme le résultat (par exemple en ne renvoyant qu'une partie), on doit forcément faire un await local pour ensuite transformer avant de renvoyer :
async function logIn(req, res) {
const { token } = await attemptLogIn(req.body)
return token
}async function process(items) {
try {
…
return await subProcess(items)
} catch (error) {
console.error(`Couldn't run subprocess for ${items}: ${error}`)
throw error // Or possibly provide a fallback value, or something.
}
}Si on peut traiter localement l'erreur que la promesse est susceptible de lever, il faut un await pour que celle-ci soit levée au sein du try…catch.
Une parallélisation n'est pas toujours préférable, mais quand elle l'est, séquencer « par défaut » laisse de la performance sur la table.
async function bulkCreateOrUpdate(data) {
…
for (let i = 0; i < data.length; i++) {
await User.upsert(data[i], { transaction })
}
…
}Cadeau bonus : ça permet dans ce cas précis de virer cette fichue boucle numérique qui aurait dû être une jolie for…of. Y'avait rien qu'allait dans ce code.
async function bulkCreateOrUpdate(data) {
…
await Promise.all(data.map((userData) => User.upsert(userData, { transaction })))
…
}Et si on est limités dans la parallélisation (ex. connexions à la base de données), pas de souci, on a des solutions pour plafonner :
import { map as cappedAll } from 'awaiting'
await cappedAll(data, 5, (userData) => User.upsert(userData, { transaction }))Non mais 🤮, quoi.
async function down(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction()
try {
await queryInterface
.bulkDelete('user_org_roles', null, {})
.then(() => queryInterface.bulkDelete('user', null, {}))
.then(() => queryInterface.bulkDelete('person', null, {}))
await transaction.commit()
} catch (error) {
await transaction.rollback()
throw error
}
}Utilise juste async / await, enfin !
async function down(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction()
try {
await queryInterface.bulkDelete('user_org_roles')
await queryInterface.bulkDelete('user')
await queryInterface.bulkDelete('person')
await transaction.commit()
} catch (error) {
await transaction.rollback()
throw error
}
}J'étais tombé sur ce multi-récidiviste :
async function deleteUser(req, res) {
return userService.destroyUser(req.params.id).then(async (user) => {
res.status(200).send(user)
})
}Purée, y'a rien qui va.
async function deleteUser(req, res) {
const user = await userService.destroyUser(req.params.id)
res.status(200).send(user)
}C'est une variante « moins grave » du mélange des styles, mais c'est quand même so 2015. Je suis tombé sur ce clusterfuck récemment :
export function findAllUsers(query) {
…
return User.findAndCountAll(…)
.then(ensureAtLeastOne)
.catch((error) => {
throw new Error(error)
})
}- Il y a un risque de double mode d’erreur (synchrone et asynchrone).
- Ce
catchest aussi utile que le H de Hawaï. - Les chaînes de promesses restent plus dures à orchestrer (pas de structures de contrôle).
function getUsersLastPost(userId) {
let user
let post
return User.findByPk(userId)
.then((u) => {
user = u
return u.posts.sort('-createdAt').findOne()
})
.then((p) => {
post = p
return p.comments.sort('-createdAt').limit(10).find()
})
.then((comments) => {
return { user, post, comments }
})
}(Je sais, je me répète.)
export async function findAllUsers(query) {
…
return ensureAtLeastOne(await User.findAndCountAll())
}
// Sans doute optimisable par eager-loading, mais c'est un autre sujet,
// et on ne fait pas de N+1 en plus ici, alors bon.
async function getUsersLastPost(userId) {
const user = await User.findByPk(userId)
const post = await user.posts.sort('-createdAt').findOne()
const comments = await post.comments.sort('-createdAt').limit(10).find()
return { user, post, comments }
}Alias « Eeeeh j'ai découvert Promise.resolve() et Promise.reject() ! »
async (error) => {
…
return Promise.reject(error)
}Mais pourquoi ?! Ta fonction async enrobe automatiquement son code en promesse. Sers-t'en !
async (error) => {
…
throw error
}Fonctions non async car elles n'utilisent pas await, mais censées renvoyer des promesses :
http.interceptors.response.use(
(response) => {
store.commit('loading/setLoading', false)
return Promise.resolve(response.data)
},
(error) => {
store.commit('loading/setLoading', false)
return Promise.reject(error)
}
)-
Le
Promise.resolveest plus explicite que de déclarer la fonctionasyncavec unreturn response.data. -
Le
Promise.rejectest plus performant que de déclarer la fonctionasyncavec unthrow error.
async/awaitest nettement supérieur aux chaînes manuellesawaitsuspend, il ne bloque pas.awaitest possible en racine de module (Top-Level Await, ou TLA) et dans le corps immédiat d'une fonctionasync.- Toute fonction peut être
async. - Les fonctions
asyncenrobent implicitement leurs corps comme promesse. - Tu ne devrais jamais faire un
return await(ou équivalent) hors d'untry…catch
.
<style> .feedback { display: flex; flex-direction: column; gap: 1em; align-items: center; } .feedback img { max-height: 7em; display: block; } .feedback p { margin: 0; } </style>Cette présentation est sur bit.ly/async-js-no-cringe.
Crédits : photo de couverture par JESHOOTS.COM sur Unsplash