Fàilte mo charaidean!
Welcome my friends!
Is mise Cory
I am Cory
Agus tha mi ag ionnsaicheadh Gàighlig.
And I am learning Scottish Gaelic
Agus a-nis, tha thu cuideachd!
And now, you are too!
'S e innleadair bathar-bog Luchd-obrach aig Aumni a th' annam.
I am a staff software engineer at Aumni
( now a JPMorgan company )
Tha mi air mo dhòigh a bhi an seo comhla ribh uile an-diugh.
I am delighted to be here with you all today.
Agus tha mi air bhioran seo a thaisbeanadh dhut...
And I am excited to present to you...
Why I Avoid
async/await
- 15 + years JavaScript and Frontend Dev.
- Sr Developer @ National Geographic
- Sr Software Engineer @ Facebook
- Standing Sr Engineer offer at AWS
- RICG contributing member
- Frontend Platform engineer @ multiple
- Planned and lead SEVERAL migration efforts to and from various tech stacks.
- Architected, built, and lead frontend stacks @ multiple orgs.
- Helping architect platform integration with JPM
A little about me
(this will be relevant later)
This ain't my first rodeo
Roughly a little more than one year ago exactly...
It went gangbusteres!!!
(for me)
Some "top" comments
"Actually" Alex
"Literally" Lars
"Humble" Henry
First off, async/await is promises!
the author [is] extrapolating from his own lack of familiarity
I think some authors need to be more humble
Some... other comments
_safety_ Socrates
Strawman Sam
"With respect" Winifred
This article is bad, bad advice.
This...
async () => {}
...is much nicer than...
() => new Promise((resolve) => resolve())
The fact that this comment has several more claps [than the article] says a lot.
"Literally" Lars
I feel bad for the author.
My mind is made up
Maaaybe I came across a bit more absolutist than I intended.
Ok,
Once more with (less) feeling.
Promises
may make for a better pattern than
In many cases
async/await
- Async/await has it's own benefits, but we will not be covering them.
- I know async/await is just "sugar" over promises.
- I know it's not a binary choice. There are some nice patterns that leverage both.
- The examples I use are absolutely representative of actual production code I have seen and see regularly.
- Fair comparisons are important to me. I work hard to make sure all my code comparisons are analogous.
Disclaimers
- Use your own critical thinking skills and assess your own practices. Don't just rely on herd mentality.
- Not an "async/await is bad and you should never use it" talk.
- Maybe I'll give that talk next year (😉)
- Maybe I'll give that talk next year (😉)
- This is a "You are probably dismissing Promise chaining too soon" talk.
Addendum
Async/Await patterns can at times provide wrong or inadequate signals that we are working with asynchronous code.
1
Wrong Signals
Async/Await incurs a high cost to doing the right thing. It encourages bad practices like "fire and forget", which neglects error handling.
2
Bad incentives
Even if you lean as far as possible into async/await, you will still need to use Promises directly at times.
3
Incomplete Abstraction
async/await
So, so many comparisons of async/await vs Promises are not comparing apples to apples, or are rigging the examples.
4
Strawman comparison
Promises
You cannot escape the fact that you are working with promises when you have to then/catch.
1
Clear Signals
Promises have a lower cost to doing the right thing, making things like error handling and scope narrowing easier.
2
Better incentives
Promises are a complete abstraction for async code. One does not have to rely on other structures or paradigms to do anything that needs to be done.
3
Single paradigm
Promises stealth teach you about functors and monads and other patterns that provide better guarantees, better reasoning, and fewer complications.
4
Gateway Drug
Mixed Signals (no, not those signals)
const howManyTruthy = async (arr) => {
const truthyValues = arr.filter(Boolean)
return truthyValues.length;
}
howManyTruthy(['', 0, 0n, false, undefined, Symbol(), null]) // 🤷♂️
What does `howManyTruthy` return?
1
2
1
Promise<1>
Clear Signals
Promises
What does `howManyTruthy` return?
1
2
1
Promise<1>
const howManyTruthy = (arr) => {
const truthyValues = arr.filter(Boolean)
return Promise.resolve(truthyValues.length);
}
howManyTruthy(['', 0, 0n, false, undefined, Symbol(), null]) // 🤷♂️
Incentives
const getData = async (userId) => {
const res = await fetch(`/data/${id}`);
return res.json());
}
async/await
Promises
const getData = (userId) => fetch(`/data/${id}`)
.then(res => res.json());
const getData = async (userId) => {
try {
res = await fetch(`/data/${id}`);
return res.json()
} catch (e) {
return {};
}
}
const getData = (userId) => fetch(`/data/${id}`)
.then(res => res.json())
.catch((e) => ({}));
Async/await imposes a stiff syntactic penalty on error handling.
Bad Incentives
The predictable result is that when times lines and pressures conspire, many devs wont deal with errors.
Async/Await + Promises
async/await + promises
const getData = async (userId) => await fetch(`/data/${id}`)
.json()
.catch((e) => ({}));
You still have to directly use promises. 🤷♂️
Which leads us directly into the next point.
Forced Multi-Paradigm
Async/await is an incomplete abstraction. We are forced to live in two worlds at the same time. Sometimes async/await, sometimes Promise chaining.
Maybe this is fine. It's certainly managable in isolation, but programming is hard. The fewer things we have to context switch on, generally, the better.
Sync vs Async vs Promise
const processData = ({ userData, sessionPreferences }) => {
save('userData', userData);
save('session', sessionPreferences);
return { userData, sessionPreferences }
}
const processData = async ({ userData, sessionPreferences }) => {
await save('userData', userData);
await save('session', sessionPreferences);
return { userData, sessionPreferences }
}
const processData = ({ userData, sessionPreferences }) =>
save('userData', userData)
.then(() => save('session', sessionPreferences))
.then(() => ({ userData, sessionPreferences })
Sync
async/await
Promises
Sync vs Async vs Promise
const processData = ({ userData, sessionPreferences }) => {
save('userData', userData);
save('session', sessionPreferences);
return { userData, sessionPreferences };
}
const processData = async ({ userData, sessionPreferences }) => {
await Promise.all([
await save('userData', userData),
await save('session', sessionPreferences),
]);
return { userData, sessionPreferences };
}
const processData = ({ userData, sessionPreferences }) =>
Promise.all([
save('userData', userData)
save('session', sessionPreferences)
]).then(() => ({ userData, sessionPreferences });
Sync
async/await
Promises
const processData = async ({ userData, sessionPreferences }) => {
await Promise.all([
save('userData', userData),
save('session', sessionPreferences),
]);
return { userData, sessionPreferences };
}
Forced Multi-Paradigm
Sometimes, we can't or shouldn't use the async/await key words when working with async functions dealing with async actions. 🤨
Context switching within the same code space is just one more thing we have to manage when ever we use async/await.
Single Paradigm
Promises are a complete abastraction over asynchronous actions. When using promises and promise chainging, we are never not doing that. One less thing to have to context switch on. 🥰
Strawman Arguments
function loadJson(url) {
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new Error(response.status);
}
});
}
async function loadJson(url) {
let response = await fetch(url);
if (response.status == 200) {
let json = await response.json();
return json;
}
throw new Error(response.status);
}
Credit https://javascript.info/task/rewrite-async
Assignment:
Solution:
🤔
🤨 huh?
Steelmanning
const loadJson = (url) => fetch(url)
.then(response => response.ok
? response.json()
: Promise.reject(response));
const loadJson = async (url) => {
const response = await fetch(url);
if (response.ok) return response.json();
throw new Error(response.status);
}
Credit https://javascript.info/task/rewrite-async
Credit https://javascript.info/task/rewrite-async
Assignment:
Solution:
😇
😆 better
const loadJson = async (url) => {
const response = await fetch(url);
if (response.ok) return response.json();
throw response;
}
Remember this?
This...
async () => {}
...is much nicer than...
() => new Promise((resolve) => resolve())
Strawman
() => new Promise((resolve) => resolve())
async () => {}
Assignment:
Solution:
🥸
🤦♂️
Steelman
Promise.resolve
🫰
Real (Sanitized) Prod Code
validateForm()
.then(isFormValid => isFormValid || Promise.reject(new Error(FormStatus.INCOMPLETE)))
.then(async () => {
const { investees, investors, hqInvestee, hqInvestor } = formatFormData(getFormValues());
const formattedInvestee = {
...investees,
industry_ids: investees.industries?.map(({ id }) => id) ?? [],
};
const formattedInvestor = {
...investors,
industry_ids: investors.industries?.map(({ id }) => id) ?? [],
};
const investeeHQ = Object.keys(hqInvestee).length ? hqInvestee : undefined;
const investorHQ = Object.keys(hqInvestor).length ? hqInvestor : undefined;
if (!formattedInvestee.id && !formattedInvestor.id) {
createLegalEntity(formattedInvestee, 'investee', investeeHQ)
.then(async investee => {
setInvestee({ ...formattedInvestee, id: investee.id });
const investor = await createLegalEntity(formattedInvestor, 'investor', investorHQ);
setInvestor({ ...formattedInvestor, id: investor.id });
return { investee, investor };
})
.then(async () => {
await submit();
})
.catch(error => showErrorMessage(error));
}
else if (!formattedInvestee.id) {
await createLegalEntity(formattedInvestee, 'investee', investeeHQ)
.then(investee => {
setInvestee('investee', { ...formattedInvestee, id: investee.id });
})
.then(async () => {
await submit();
})
.catch(error => showErrorMessage(error));
}
else if (!formattedInvestor.id) {
await createLegalEntity(formattedInvestor, 'investor', investorHQ)
.then(investor => {
setInvestor({ ...formattedInvestor, id: investor.id });
})
.then(async () => {
await submit();
})
.catch(error => showErrorMessage(error));
}
else {
await submit();
}
})
.catch(error => {
if (error.message === FormStatus.INCOMPLETE) {
showErrorMessage(error);
} else {
reportError(error);
showErrorMessage({ message: 'There was an adding the record, please try again.' });
}
});
Async/Await only
try {
await const isValidForm = validateForm()
if (!isFormValid) throw new Error(FormStatus.INCOMPLETE)
const { investees, investors, hqInvestee, hqInvestor } = formatFormData(getFormValues());
const formattedInvestee = {
...investees,
industry_ids: investees.industries?.map(({ id }) => id) ?? [],
};
const formattedInvestor = {
...investors,
industry_ids: investors.industries?.map(({ id }) => id) ?? [],
};
const investeeHQ = Object.keys(hqInvestee).length ? hqInvestee : undefined;
const investorHQ = Object.keys(hqInvestor).length ? hqInvestor : undefined;
if (!formattedInvestee.id && !formattedInvestor.id) {
try {
const investee = await createLegalEntity(formattedInvestee, 'investee', investeeHQ)
setInvestee({ ...formattedInvestee, id: investee.id });
const investor = await createLegalEntity(formattedInvestor, 'investor', investorHQ);
setInvestor({ ...formattedInvestor, id: investor.id });
await submit();
} catch (error) {
showErrorMessage(error)
}
}
else if (!formattedInvestee.id) {
try {
const investee = await createLegalEntity(formattedInvestee, 'investee', investeeHQ)
setInvestee('investee', { ...formattedInvestee, id: investee.id });
await submit();
} catch (error) {
showErrorMessage(error)
}
}
else if (!formattedInvestor.id) {
try {
const investor = await createLegalEntity(formattedInvestor, 'investor', investorHQ)
setInvestor({ ...formattedInvestor, id: investor.id });
await submit();
} catch (error) {
showErrorMessage(error)
}
}
else {
await submit();
}
} catch (error) {
if (error.message === FormStatus.INCOMPLETE) {
showErrorMessage(error);
} else {
reportError(error);
showErrorMessage({ message: 'There was an adding the record, please try again.' });
}
}
Promise Chaining
const rejectWithWhen = rejection => predicate => ctx => predicate(ctx) ? rejection(ctx) : ctx;
const rejectWithIncompleteWhen = rejectWithWhen(new Error(FormStatus.INCOMPLETE));
validateForm()
.then(rejectWithIncompleteWhen(isFormValid => !isFormValid))
.then(() => formatFormData(getFormValues()))
.then(({ investees, investors, hqInvestee, hqInvestor }) => ({
investee: { ...investees, industry_ids: investees.industries?.map(get('id')) ?? [] },
investor: { ...investors, industry_ids: investors.industries?.map(get('id')) ?? [] },
investeeHQ: Object.keys(hqInvestee).length ? hqInvestee : undefined,
investorHQ: Object.keys(hqInvestor).length ? hqInvestor : undefined,
}))
.then(({ investee, investor, investeeHQ, investorHQ }) => Promise.allSettled([
investee.id
? investee
: createLegalEntity(investee, 'investee', investeeHQ).then(tap(({ id }) => setInvestee({ ...investee, id }))),
investor.id
? investor
: createLegalEntity(investor, 'investor', investorHQ).then(tap(({ id }) => setInvestor({ ...investor, id }))),
]))
.then(results =>
results.every(({ status }) => status === 'fulfilled')
? submit()
: Promise.reject(results.filter(({ status }) => status === 'rejected')),
)
.catch(errors => {
errors.forEach(error => {
if (error.message !== FormStatus.INCOMPLETE) reportError(error);
showErrorMessage({
message:
error.message === FormStatus.INCOMPLETE ? error.message : 'There was an error adding the record, please try again.',
});
});
});
Scope Isolation
const rejectWithWhen = rejection => predicate => ctx => predicate(ctx) ? rejection(ctx) : ctx;
const rejectWithIncompleteWhen = rejectWithWhen(new Error(FormStatus.INCOMPLETE));
validateForm()
.then(rejectWithIncompleteWhen(isFormValid => !isFormValid))
.then(() => formatFormData(getFormValues()))
.then(({ investees, investors, hqInvestee, hqInvestor }) => ({
investee: { ...investees, industry_ids: investees.industries?.map(get('id')) ?? [] },
investor: { ...investors, industry_ids: investors.industries?.map(get('id')) ?? [] },
investeeHQ: Object.keys(hqInvestee).length ? hqInvestee : undefined,
investorHQ: Object.keys(hqInvestor).length ? hqInvestor : undefined,
}))
.then(({ investee, investor, investeeHQ, investorHQ }) => Promise.allSettled([
investee.id
? investee
: createLegalEntity(investee, 'investee', investeeHQ).then(tap(({ id }) => setInvestee({ ...investee, id }))),
investor.id
? investor
: createLegalEntity(investor, 'investor', investorHQ).then(tap(({ id }) => setInvestor({ ...investor, id }))),
]))
.then(results =>
results.every(({ status }) => status === 'fulfilled')
? submit()
: Promise.reject(results.filter(({ status }) => status === 'rejected')),
)
.catch(errors => {
errors
.map(tap(error => error.message !== FormStatus.INCOMPLETE && reportError(error)))
.map(tap(({ message }) => showErrorMessage({
message: message === FormStatus.INCOMPLETE ? message : 'There was an error adding the record.'
})));
});
Scope Isolation
validateForm()
.then(rejectWithIncompleteWhen(isFormValid => !isFormValid))
.then(() => formatFormData(getFormValues()))
.then(({ investees, investors, hqInvestee, hqInvestor }) => ({
formattedInvestee: { ...investees, industry_ids: investees.industries?.map(get('id')) ?? [] },
formattedInvestor: { ...investors, industry_ids: investors.industries?.map(get('id')) ?? [] },
investeeHQ: Object.keys(hqInvestee).length ? hqInvestee : undefined,
investorHQ: Object.keys(hqInvestor).length ? hqInvestor : undefined,
}))
.then(results =>
results.every(({ status }) => status === 'fulfilled')
? submit()
: Promise.reject(results.filter(({ status }) => status === 'rejected')),
)
.catch(errors => {
errors
.map(tap(error => error.message !== FormStatus.INCOMPLETE && reportError(error)))
.map(tap(({ message }) => showErrorMessage({
message: message === FormStatus.INCOMPLETE ? message : 'There was an error adding the record.'
})));
});
.then(({ investee, investor, investeeHQ, investorHQ }) => Promise.allSettled([
investee.id
? investee
: createLegalEntity(investee, 'investee', investeeHQ)
.then(tap(({ id }) => setInvestee({ ...investee, id }))),
investor.id
? investor
: createLegalEntity(investor, 'investor', investorHQ)
.then(tap(({ id }) => setInvestor({ ...investor, id }))),
]))
Scope Isolation
const rejectWithWhen = rejection => predicate => ctx => predicate(ctx) ? rejection(ctx) : ctx;
const rejectWithIncompleteWhen = rejectWithWhen(new Error(FormStatus.INCOMPLETE));
validateForm()
.then(rejectWithIncompleteWhen(isFormValid => !isFormValid))
.then(() => formatFormData(getFormValues()))
.then(({ investees, investors, hqInvestee, hqInvestor }) => ({
investee: { ...investees, industry_ids: investees.industries?.map(get('id')) ?? [] },
investor: { ...investors, industry_ids: investors.industries?.map(get('id')) ?? [] },
investeeHQ: Object.keys(hqInvestee).length ? hqInvestee : undefined,
investorHQ: Object.keys(hqInvestor).length ? hqInvestor : undefined,
}))
.then(({ investee, investor, investeeHQ, investorHQ }) => Promise.allSettled([
investee.id
? investee
: createLegalEntity(investee, 'investee', investeeHQ).then(tap(({ id }) => setInvestee({ ...investee, id }))),
investor.id
? investor
: createLegalEntity(investor, 'investor', investorHQ).then(tap(({ id }) => setInvestor({ ...investor, id }))),
]))
.then(results =>
results.every(({ status }) => status === 'fulfilled')
? submit()
: Promise.reject(results.filter(({ status }) => status === 'rejected')),
)
.catch(errors => {
errors
.map(tap(error => error.message !== FormStatus.INCOMPLETE && reportError(error)))
.map(tap(({ message }) => showErrorMessage({
message: message === FormStatus.INCOMPLETE ? message : 'There was an error adding the record.'
})));
});
One more thing.
!
Using promises is known to the state of California to cause interest in and comprehension of concepts in
FUNCTIONAL PROGRAMMING!!!
- Promise chaining helps to train your brain in the direction of functional programming.
- Promise chaining is similar in kind to Functor mapping and pipelining.
- Promises are instructive in understanding Monads.
Functional Programming
Why Promises
1
Clear Signals
2
Better incentives
3
Single paradigm
4
Learn FP
Tha mi an dòchas gum bi latha math leibh.
I hope you have a good day.
Tapadh leibh
Thank you.
In Many Cases, Promises May be a better solution than async/await
By Cory Brown
In Many Cases, Promises May be a better solution than async/await
- 226