JavaScript Promises: Beginner to Advanced

L&D Oct. 2020
Steve Olsen

GOALS:

  • To learn something new
  • Smile twice (or more)

Yup, it's normal.

Promises 101

What?... Why?
Async code sucked...

Once upon a time,

Call Back Hell (CBH)

const delayCallback = (ms, cb) => {
  console.log('Delayed Callback', {ms})
  setTimeout(cb, ms)
}

const addCallback = (a, b, cb) => {
  const result = a + b

  // This is SYNCHRONOUS style:
  // return result

  // But we want ASYNCHRONOUS style:
  // cb(result)
  // ...With a delay:
  delayCallback(100,
      () => cb(result)
  )
}

addCallback(1, 2, 
    (result1) => addCallback(result1, result1, 
        (result2) => console.log('callback:', result2)
            // ....
                // ....
    )
)

Promises would make this better*.

*Smart people promised us

Promise: a container for a value,
accessed via a callback
("monad" or "monoid" in FP)
A Promise has state:
  1. pending
  2. resolved OR rejected

My 1st Promise Chain

const myPromise = new Promise(
	(resolve, reject) => {
		resolve(42)
	}
)

myPromise
.then(
	(value) => { 
		console.log(value)
		return value + 1 // Tip: usually want to return a chained value!
    }
)
.then((value2) => console.log(value2))

Doin' Somethin' Realz

const delay = (ms) => new Promise(resolve => {
    setTimeout(() => resolve(), ms)
})

const addAsync = (a, b) => {
    return new Promise((resolve, reject) => { // create our own Promise
        const result = a + b
        delay(1000)
            .then(() => resolve(result))
    })
}

addAsync(10, 20)
	.then(result1 => addAsync(result1, result1))
	.then(result2 => console.log(result2))



// Other promise helpers/shortcuts
Promise.resolve(1)
Promise.reject(new Error())
Promise.all([promise1, promise2])

Amazing!

Nothing bad can happen??

Promises 201

Pesky Errors...

Error handling: direct try/catch/finally

try {
  (null).prop
} catch (error) {
  console.error(error)
}

try {
  throw new Error('my custom error')
} catch (error) {
  console.error(error)
}

const addCallback = (a, b, cb) => {
    const result = a + b
    // ...oops!
    throw new Error('bad calulator CPU! 1')
}

try {
  addCallback(1, 2, console.log)
} catch (error) {
  console.error(error)
}

Error handling: promise try/catch/finally

const add = (a, b) => {
    return new Promise((resolve, reject) => {
        const result = a + b
        // ...oops!
        reject(new Error('bad calulator board 2'))
    })
}

add(10, 20)
 	.then(r => console.log(r))
	.catch(error => console.error(error))
	.then(() => console.log('carry on!'))

// finally {....}
// .finally(() => {....})

Spot the bugs

const value = 10
const finalValue = Promise.resolve(value)
	.then(val1 => val + 10)
	.then(val2 => { 
		console.log(val2)
	})
	.then(val2 => { val2 - 5 })
    
console.log(finalValue)

Spot the bugs

const value = 10
const finalValue = Promise.resolve(value)
	.then(val1 => val + 10)
	.then(val2 => { 
		Promise.reject(new Error("no way! " + val2))
	})
	.catch(error => console.log(error.message))
	.then(() => { value }) // set to default value
    
console.log(finalValue)

Promises 301

Await there, just a minute you!

async function sugar:

  • It CAN await a Promise for its value
  • It MUST return a Promise
  • (A lot of "autoboxing" happens to enforce these rules)
async function main() {
    ...
}
main()

async / await syntax

const add = async (a, b) => {
    return new Promise(
        async (resolve, reject) => { // create our own Promise
            const result = a + b
            await delay(100) // like "sleep"
            resolve(result)
        }
    )
}

async function main() {
    const r1 = await add(40, 40)
    const r2 = await add(r1, r1)
    console.log('async/await:', r1, r2)
}
main()

async / await errors

async function work() {
    let result
    try {
        result = await add(1, 2) // don't forget the await !!
    } catch (error) {
        console.error(error)
    }
    console.log('carry on! 5', result)
}
work()

async / await mix-n-match

Promise.then / Promise.catch
async function work3() {
    let result
    try {
        result = await add(1, 2)
                    .catch(
                    	() => "Add Error converted into a String!"
                    )
    } catch (error) {
        console.error('try/catch:', error) // never runs!
    }
    console.log('carry on!', result)
}
work3()

async / await mix-n-match

Promise.then / Promise.catch
async function work2() {
    let result
    try {
        result = await add(1, 2)
                    .catch((error) => {
                        console.error('.catch', error)
                        return Promise.reject(error)
                    })
    } catch (error) {
        console.error('try/catch:', error)
    }
    console.log('carry on!', result)
}
work2()

Spot the bugs

const myFunc = async () => {
	const value = Promise.resolve(10)
	const value2 = await (value + 10)
	if (value > 10) {
		Promise.reject(new Error("too big!"))
	}
}

try {
	myFunc()
} catch (err) {
	console.error("I want to catch an error here!", err.message)
}

BONUS: Autoboxing

// We've seen autoboxing already:
myVar = Promise.resolve(1).then(v => v + 1) 
// Q: result is 2, or Promise<2>?
myVar = Promise.resolve(1).then(v => Promise.resolve(v + 1)) // same

myVar = await 4 
// Q: myVar1 is 4, or Promise<4>?
myVar = await Promise.resolve(4) // same

myVar = Promise.resolve(Promise.resolve(1)) 
// Q: myVar is Promise<1>, or Promise<Promise<1>>?

myVar = await Promise.resolve(Promise.resolve(1)) 
// Q: myVar is 1, or Promise<1>?

// not so for Promise.reject(Promise.reject(...)) - don't mess with reject!

myVar = await Promise.resolve(Promise.reject(new Error('message')))
throw new Error('message') // same

Callback vs. Promises (APIs)

Can't we all just get along!

Let's suppose:

Your API: callback Your API: Promise
Their API: callback #A #B
Their API: Promise #C #D

#A

// MyCallback + Callback
function getData(id, cb) {
   api('/user/' + id, 
       function(err, data) {
           if (err) cb(err);
           else cb(null, data);
       }
   );
}

#B (most common)

// MyPromise + Callback
async function getData(id) {
  return new Promise(function(resolve, reject) {
    api('/user/' + id, 
        function(err, data) {
            if (err) reject(err);
            else resolve(data);
        }
    );
  });
}

#C

// MyCallback + Promise
function getData(id, cb) {
  api('/user/' + id)
    .then(function (data) { cb(null, data) })
    .catch(function (err) { cb(err) });
}

#D (ahhh.....!)

// MyPromise + Promise
async function getData(id) {
   return api('/user/' + id);
}

The Mental Model

Are these threads, or not?!

JS Event Loop = single threaded

Promises = single threaded

Threads Recap

Think of Promise Chains as:

Single-Core Threading!

Two Distinct Patterns

of Threading:

  1. Background (fire-and-forget)
    • Do whatchawanna
  2. Synchronous (join)
    • I must wait for you to finish first

It's the same for Promises!

("You don't have to await every Promise")

Scenario:

  • You are calling an API for data
  • Pattern: join (await)
async function getArticleDate(id) {
    const data = await callDatabase(id)
    const time = new Date(data.timestamp)
    return time
}

Scenario:

  • You aren't sure if the value is a Promise or not
  • Pattern: join safely (await)
function randomNumberReturn() {
    if (Math.random() < 0.5) {
        return 42
    } else if (Math.random() >= 0.5) {
        return Promise.resolve(42)
    }
    // could return undefined!!
}

async function work() {
    const data = await randomNumberReturn() // might be a promise
    return data * 2
}

Scenario:

  • You are sending telemetry/analytics
  • Pattern: fire-and-forget (don't await)
async function sendTelemetry(eventName, eventValue) {
    const config = await getRemoteSecret('GoogleAnalytics')
    const key = config.key
    sendSecureTelemetryEvent(key, eventName, eventValue)
    	.catch((err) => console.log("This is bad, I can't do much about it!"))
}

async function getArticleDate(id) {
    const data = await callDatabase(id)
    const time = new Date(data.timestamp)
    sendTelemetry(id + "_time", time) // don't await this thread!
    return time
}

note: It can't throw an Error

Just because it's a Promise

Doesn't mean you have to wait for it.

But you might choose to await....

so other threads don't starve.

Advanced Mode

Turn it up to 11

The Achilles Heel

"thread starvation"

Thread 1                               Thread 2

Microtasks vs. EventLoop

Priority!
(Promises)

Normal

(Timers/IO)

Execution Order

new Promise(async (res) => { 
   console.log('in promise callback 1');
   await 1; // this seems unimportant.... is it??
   console.log('in promise callback 2');
})

console.log('after Promise instantiated');
setTimeout(() => { console.log('TIMER DONE!', v) }, 1)

const p1 = Promise.resolve(1)
p1
.then((v) => v)
.then((v) => v)
.then((v) => { console.log('PROMISE1 DONE!', v) })

const p2 = Promise.resolve(2)
p2
.then((v) => v)
.then((v) => { console.log('PROMISE2 DONE!', v) })

Shared State

let shared = ''

function threadA() {
  new Promise(async res => {
    shared = 'a'
    shared = await Promise.resolve('a')
    shared = await 'a'
    await Promise.resolve()
    console.log('a ??', shared)
  })
}

function threadB() {
  new Promise(async res => {
    shared = 'b'
    shared = await Promise.resolve('b')
    shared = await 'b'
    await Promise.resolve()
    console.log('b ??', shared)
  })
}

threadA()
threadB()

Sync and Parallel Threads

const t1 = () => Promise.resolve(1) // eg. fetch()
const t2 = async () => 2 // eg. fetch()
const t3 = async () => 3 // eg. fetch()

// parallel A
const allResults = await Promise.all(
    [t1(), t2(), t3()]
)

// parallel B
const workQueue = [t1, t2, t3]
const allResults = await Promise.all(
    workQueue.map(t => t())
)

// synchronous A
for (let t of [t1, t2, t3]) {
    const result = await t()
}

// synchronous B
// can't use .forEach(), 
// beacuse you can't get the previous Promise to await a resolve
[t1, t2, t3].reduce(async (prevPromise, t) => {
    const prevResult = await prevPromise
    // handle result here
    return t()
}, Promise.resolve())

!Oh No!

We've been Tweet Rushed...

Network Stampede - Cache

Network Stampede - Cache

Dictionary <key: Promise>

Network Stampede - Cache

const promiseCache = {}

async function getCachedData(key) {
    if(!promiseCache[key]) {
        promiseCache[key] = new Promise((resolve, reject) => {
            getDataFromDatasource(key) // request goes to DB, returns Promise
                .then((responseData) => { resolve(responseData) })
                .catch((error) => { reject(error) })
                .finally(async () => {
                    await delay(1000)
                    delete promiseCache[key]
                });
            
        })
    }
    return promiseCache[key]
}

async function getCachedData2(key) {
    if(!promiseCache[key]) {
        promiseCache[key] = getDataFromDatasource(key)
            .finally(async () => {
                await delay(1000)
                delete promiseCache[key]
            });
        }
    }
    return promiseCache[key]
}

Common code scenarios:

  • Caching API response data
    • TTL or LRU
    • >>>Dictionary/Object collection
  • Throttling API calls
    • Avoid DOS your own servers!
    • 3rd Party API rate limits
    • >>>Queue/Array collection + delay
  • Signal (un)finished thread of work
    • Timeout with auto reject
  • Anything related to I/O!

fetch network data

fetch(url, opts)

  .then(response => response.json())

  .then(result => { ... })

Util Libraries

BONUS FEATURE:

Generators

(Precursor to Async/Await)

(Come and learn, later on...)

Generator Functions

function* fourNumbers() {
    let count = 0
    while (count < 3) {
       yield count
       count += 1
    }
    return 99
}

const myGenerator = fourNumbers()
console.log(myGenerator.next()) // 1
console.log(myGenerator.next()) // 2
console.log(myGenerator.next()) // 3
console.log(myGenerator.next()) // 4
console.log(myGenerator.next()) // 5

/*
{value: 0, done: false}
{value: 1, done: false}
{value: 2, done: false}
{value: 99, done: true}
{value: undefined, done: true}
*/

Async Generator Functions

async function* fourNumbers() {
    let count = 1
    while (count < 4) {
       yield count
       count += 1
    }
    return 99
}

const myGenerator = fourNumbers()
console.log(await myGenerator.next()) // 1
console.log(myGenerator.next()) // 2
console.log(myGenerator.next()) // 3
console.log(myGenerator.next()) // 4
console.log(myGenerator.next()) // 5

/*
{value: 1, done: false}
Promise<{value: 2, done: false}>
Promise<{value: 3, done: false}>
Promise<{value: 99, done: true}>
Promise<{value: undefined, done: true}>
*/

BONUS FEATURE:

Tag functions

(Arguments from a template literal)

(Come and learn, later on...)

Examples

// TYPICAL FUNCTION:
function greet(...values){};

// TAG FUNCTION DEFINITION
function greet([tokens, ...values]){
	console.log(tokens); // ["I'm ", ". I'm ", " years old."]
	console.log(values); // [name, age]
};

// TAG FUNCTION CALL:
greet`I am ${name}. I am ${age} years old.`;
// EQUIVALENT FUNCTION CALL:
greet(["I'm ", ". I'm ", " years old."], name, age);

Did you learn something new?

Did you smile twice?

THANK YOU

Promises are Powerful.

Promises are Simple.

Promises are Fun.

I promise.

Questions?

L&D JavaScript Promises: Beginner to Advanced

By solsen-tl

L&D JavaScript Promises: Beginner to Advanced

  • 45