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 (😉)
       
  • 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

  • 205