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:
-
pending
-
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:
-
Background (fire-and-forget)
- Do whatchawanna
-
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