Slide 1

Slide 1 text

ASYNC JAVASCRIPT ASYNC JAVASCRIPT PATTERNS AND GOTCHAS ☔ PATTERNS AND GOTCHAS ☔

Slide 2

Slide 2 text

Hugo Di Francesco Engineer at Concrete.cc Slides: Twitter: codewithhugo.com/async-js @hugo__df

Slide 3

Slide 3 text

CONTENTS CONTENTS 1. Asynchronicity in JavaScript (a history lesson) 2. Why async/await? 3. Gotchas 4. Patterns

Slide 4

Slide 4 text

ASYNCHRONICITY IN JAVASCRIPT ASYNCHRONICITY IN JAVASCRIPT Primitives: Callbacks Promises (Observables) async/await

Slide 5

Slide 5 text

What's asynchronous in a web application?

Slide 6

Slide 6 text

What's asynchronous in a web application? tl;dr Most things

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

1. any network calls (HTTP, database)

Slide 9

Slide 9 text

1. any network calls (HTTP, database) 2. timers (setTimeout, setInterval)

Slide 10

Slide 10 text

1. any network calls (HTTP, database) 2. timers (setTimeout, setInterval) 3. filesystem access

Slide 11

Slide 11 text

1. any network calls (HTTP, database) 2. timers (setTimeout, setInterval) 3. filesystem access ... Anything else that can be offloaded

Slide 12

Slide 12 text

In JavaScript, these operations are non-blocking. HTTP Request in Python: HTTP Request in JavaScript: data = request(myUrl) print(data) request(myUrl, (err, data) => { console.log(data); });

Slide 13

Slide 13 text

Why non-blocking I/O? You don't want to freeze your UI while you wait.

Slide 14

Slide 14 text

Why non-blocking I/O? You don't want to freeze your UI while you wait. Non-blocking -> waiting doesn't cost you compute cycles.

Slide 15

Slide 15 text

How non-blocking I/O is implemented (in JavaScript): pass a "callback" function it's called with the outcome of the async operation

Slide 16

Slide 16 text

NODE-STYLE CALLBACKS NODE-STYLE CALLBACKS myAsyncFn((err, data) => { if (err) dealWithIt(err); doSomethingWith(data); })

Slide 17

Slide 17 text

A callback is: "just" a function in examples, usually anonymous functions (pass function () {} directly) according to some style guides, should be an arrow function (() => {}) called when the async operation finishes

Slide 18

Slide 18 text

A Node-style callback is: called with any error(s) as the first argument/parameter, if there's no error, null is passed called with any number of "output" data as the other arguments ie. (err, data) => { /* more logic */ }

Slide 19

Slide 19 text

NODE-STYLE CALLBACKS: NODE-STYLE CALLBACKS: PROBLEMS PROBLEMS

Slide 20

Slide 20 text

1. CALLBACK 1. CALLBACK HELL HELL ⏫ ⏫ myAsyncFn((err, data) => { if (err) handle(err) myOtherAsyncFn(data, (err, secondData) => { fun(data, secondData, (err) => { if (err) handle(err) }) fn(data, secondData, (err) => { if (err) handle(err) }) }) })

Slide 21

Slide 21 text

For each asynchronous operation: extra level of indent lots of names for async output: data, secondData

Slide 22

Slide 22 text

2. SHADOWING VARIABLES 2. SHADOWING VARIABLES err (in myAsyncFn callback) !== err (in myOtherAsyncFn callback) despite having the same name myAsyncFn((err, data) => { if (err) handle(err) myOtherAsyncFn(data, (err, secondData) => { fun(data, secondData, (err) => { if (err) handle(err) }) fn(data, secondData, (err) => { if (err) handle(err) }) }) })

Slide 23

Slide 23 text

3. DUPLICATED ERROR HANDLING ❄ 3. DUPLICATED ERROR HANDLING ❄ 1 call to handle(err) per operation myAsyncFn((err, data) => { if (err) handle(err) myOtherAsyncFn(data, (err, secondData) => { fun(data, secondData, (err) => { if (err) handle(err) }) fn(data, secondData, (err) => { if (err) handle(err) }) }) })

Slide 24

Slide 24 text

4. SWALLOWED ERRORS 4. SWALLOWED ERRORS Ideal failure: fail early fail fast fail loud

Slide 25

Slide 25 text

Spot the unhandled error: myAsyncFn((err, data) => { if (err) handle(err) myOtherAsyncFn(data, (err, secondData) => { fun(data, secondData, (err) => { if (err) handle(err) }) fn(data, secondData, (err) => { if (err) handle(err) }) }) })

Slide 26

Slide 26 text

Silent error err doesn't get handled hope your linter caught that myAsyncFn((err, data) => { if (err) handle(err) myOtherAsyncFn(data, (err, secondData) => { // Missing error handling! fun(data, secondData, (err) => { if (err) handle(err) }) fn(data, secondData, (err) => { if (err) handle(err) }) }) })

Slide 27

Slide 27 text

CALLBACK PROBLEMS CALLBACK PROBLEMS 1. Callback hell (indents ) 2. Shadowed variables 3. Duplicated error-handling 4. Swallowed errors

Slide 28

Slide 28 text

BRING ON THE PROMISE BRING ON THE PROMISE

Slide 29

Slide 29 text

myAsyncFn() .then((data) => Promise.all([ data, myOtherAsyncFn(data), ])) .then(([data, secondData]) => Promise.all([ fun(data, secondData), fn(data, secondData), ])) .then(/* do anything else */) .catch((err) => handle(err));

Slide 30

Slide 30 text

Pros: Chainable no crazy indent stuff myAsyncFn() .then((data) => Promise.all([ data, myOtherAsyncFn(data), ])) .then(([data, secondData]) => Promise.all([ fun(data, secondData), fn(data, secondData), ])) .then(/* do anything else */) .catch((err) => handle(err));

Slide 31

Slide 31 text

Pros: Single error handler .catch once on the chain myAsyncFn() .then((data) => Promise.all([ data, myOtherAsyncFn(data), ])) .then(([data, secondData]) => Promise.all([ fun(data, secondData), fn(data, secondData), ])) .then(/* do anything else */) .catch((err) => handle(err));

Slide 32

Slide 32 text

Pros: lots of tightly scoped functions Small functions are usually easier to understand myAsyncFn() .then((data) => Promise.all([ data, myOtherAsyncFn(data), ])) .then(([data, secondData]) => Promise.all([ fun(data, secondData), fn(data, secondData), ])) .then(/* do anything else */) .catch((err) => handle(err));

Slide 33

Slide 33 text

Cons: Lots of tightly scoped functions Very verbose way of returning multiple things. .then((data) => Promise.all([ data, myOtherAsyncFn(data), ])

Slide 34

Slide 34 text

PROMISE GOTCHAS PROMISE GOTCHAS

Slide 35

Slide 35 text

Gotcha: ♀ nesting them is super tempting. myAsyncFn() .then((data) => myOtherAsyncFn(data) .then( ([data, secondData]) => Promise.all([ fun(data, secondData), fn(data, secondData), ]) ) ) .catch((err) => handle(err))

Slide 36

Slide 36 text

Solution: Avoid the Pyramid of Doom ☠ Promises "flatten": you can return a Promise from a then and keep chaining myAsyncFn() .then((data) => Promise.all([ data, myOtherAsyncFn(data), ])) .then(([data, secondData]) => Promise.all([ fun(data, secondData), fn(data, secondData), ])) .then(/* do anything else */) .catch((err) => handle(err))

Slide 37

Slide 37 text

Gotcha: onRejected callback The following works: But we're back to doing per-operation error-handling like in callbacks (potentially swallowing errors etc.) myAsyncFn() .then( (data) => myOtherAsyncFn(data), (err) => handle(err) );

Slide 38

Slide 38 text

Solution: avoid it, in favour of .catch Unless you specifically need it myAsyncFn() .then( (data) => myOtherAsyncFn(data) ) .catch((err) => handle(err));

Slide 39

Slide 39 text

PROMISE RECAP PROMISE RECAP Lots of tightly scoped functions Very verbose way of returning/passing multiple things fn() .then((data) => Promise.all([ data, myOtherAsyncFn(data), ])) .then(([data, secondData]) => {})

Slide 40

Slide 40 text

ASYNC/AWAIT ASYNC/AWAIT

Slide 41

Slide 41 text

(async () => { try { const data = await myAsyncFn(); const secondData = await myOtherAsyncFn(data); const final = await Promise.all([ fun(data, secondData), fn(data, secondData), ]); /* do anything else */ } catch (err) { handle(err); } })();

Slide 42

Slide 42 text

Given a Promise (or any object that has a .then function), await takes the value passed to the callback in .then

Slide 43

Slide 43 text

await can only be used inside a function that is async * * top-level (ie. outside of async functions) await is coming (async () => { console.log('Immediately invoked function expressions (IIFEs const res = await fetch('https://jsonplaceholder.typicode.co const data = await res.json() console.log(data) })() // SyntaxError: await is only valid in async function const res = await fetch( 'https://jsonplaceholder.typicode.com/todos/2' )

Slide 44

Slide 44 text

async functions are "just" Promises const arrow = async () => { return 1 } const implicitReturnArrow = async () => 1 const anonymous = async function () { return 1 } async function expression () { return 1 } console.log(arrow()); // Promise { 1 } console.log(implicitReturnArrow()); // Promise { 1 } console.log(anonymous()); // Promise { 1 } console.log(expression()); // Promise { 1 }

Slide 45

Slide 45 text

LOOP THROUGH SEQUENTIAL CALLS LOOP THROUGH SEQUENTIAL CALLS

Slide 46

Slide 46 text

With async/await: With promises: async function fetchSequentially(urls) { for (const url of urls) { const res = await fetch(url); const text = await res.text(); console.log(text.slice(0, 100)); } } function fetchSequentially(urls) { const [ url, ...rest ] = urls fetch(url) .then(res => res.text()) .then(text => console.log(text.slice(0, 100))) .then(fetchSequentially(rest)); }

Slide 47

Slide 47 text

SHARE DATA BETWEEN CALLS SHARE DATA BETWEEN CALLS We don't have the whole async function run() { const data = await myAsyncFn(); const secondData = await myOtherAsyncFn(data); const final = await Promise.all([ fun(data, secondData), fn(data, secondData), ]); return final } .then(() => Promise.all([dataToPass, promiseThing])) .then(([data, promiseOutput]) => { })

Slide 48

Slide 48 text

ERROR HANDLING ERROR HANDLING async function withErrorHandling(url) { try { const res = await fetch(url); const data = await res.json(); return data } catch(e) { console.log(e.stack) } } withErrorHandling( 'https://jsonplaceholer.typicode.com/todos/2' // The domain should be jsonplaceholder.typicode.com ).then(() => { /* but we'll end up here */ })

Slide 49

Slide 49 text

CONS OF CONS OF async async/ /await await Browser support is only good in latest/modern browsers polyfills (async-to-gen, regenerator runtime) are sort of big supported in Node 8+ though ♀ Keen functional programming people would say it leads to a more "imperative" style of programming

Slide 50

Slide 50 text

GOTCHAS GOTCHAS

Slide 51

Slide 51 text

CREATING AN ERROR CREATING AN ERROR throw-ing inside an async function and Promise.reject work the same .reject and throw Error objects please async function asyncThrow() { throw new Error('asyncThrow'); } function rejects() { return Promise.reject(new Error('rejects')) } async function swallowError(fn) { try { await asyncThrow() } catch (e) { console.log(e.message, e.__proto__) } try { await rejects() } catch (e) { console.log(e.message, e.__proto__) } } swallowError() // asyncThrow Error {} rejects Error {}

Slide 52

Slide 52 text

WHAT HAPPENS WHEN YOU FORGET WHAT HAPPENS WHEN YOU FORGET await await? ? values are undefined TypeError: x.fn is not a function async function forgotToWait() { try { const res = fetch('https://jsonplaceholer.typicode.com/tod const text = res.text() } catch (e) { console.log(e); } } forgotToWait() // TypeError: res.text is not a function

Slide 53

Slide 53 text

WHAT HAPPENS WHEN YOU FORGET WHAT HAPPENS WHEN YOU FORGET await await? ? console.log of Promise/async function *inserts 100th reminder*: an async function is a Promise async function forgotToWait() { const res = fetch('https://jsonplaceholer.typicode.com/todos console.log(res) } forgotToWait() // Promise { }

Slide 54

Slide 54 text

PROMISES EVALUATE EAGERLY ✨ PROMISES EVALUATE EAGERLY ✨ Promises don't wait for anything to execute, when you create it, it runs: new Promise((resolve, reject) => { console.log('eeeeager'); resolve(); })

Slide 55

Slide 55 text

TESTING GOTCHAS TESTING GOTCHAS Jest supports Promises as test output (therefore also async functions) what if your test fails? const runCodeUnderTest = async () => { throw new Error(); }; test('it should pass', async () => { doSomeSetup(); await runCodeUnderTest(); // the following never gets run doSomeCleanup(); })

Slide 56

Slide 56 text

TESTING GOTCHAS TESTING GOTCHAS BUT do your cleanup in "before/a er" hooks, async test bodies crash and don't clean up which might make multiple tests fail describe('feature', () => { beforeEach(() => doSomeSetup()) afterEach(() => doSomeCleanup()) test('it should pass', async () => { await runCodeUnderTest(); }) })

Slide 57

Slide 57 text

PATTERNS PATTERNS A lot of these are to avoid the pitfalls we've looked in the "gotchas" section.

Slide 58

Slide 58 text

RUNNING PROMISES IN PARALLEL RUNNING PROMISES IN PARALLEL Promise.all function fetchParallel(urls) { return Promise.all( urls.map( (url) => fetch(url).then(res => res.json()) ) ); }

Slide 59

Slide 59 text

RUNNING PROMISES IN PARALLEL RUNNING PROMISES IN PARALLEL Promise.all + map over an async function Good for logging or when you've got non- trivial/business logic function fetchParallel(urls) { return Promise.all( urls.map(async (url) => { const res = await fetch(url); const data = await res.json(); return data; }) ); }

Slide 60

Slide 60 text

DELAY EXECUTION OF A PROMISE ✋ DELAY EXECUTION OF A PROMISE ✋ Promises are eager, they just wanna run! Use a function that returns the Promise No Promise, no eager execution Fancy people call the above "thunk" function getX(url) { return fetch(url) } // or const delay = url => fetch(url)

Slide 61

Slide 61 text

SEPARATE SYNCHRONOUS AND SEPARATE SYNCHRONOUS AND ASYNCHRONOUS OPERATIONS ASYNCHRONOUS OPERATIONS async fetch > do stuff in memory > async write back const fs = require('fs').promises const fetchFile = () => fs.readFile('path', 'utf-8'); const replaceAllThings = (text) => text.replace(/a/g, 'b'); const writeFile = (text) => fs.writeFile('path', text, 'utf-8'); (async () => { const text = await fetchFile(); const newText = replaceAllThings(text); await writeFile(newText); })();

Slide 62

Slide 62 text

RUNNING PROMISES SEQUENTIALLY RUNNING PROMISES SEQUENTIALLY using recursion + rest/spread and way too much bookkeeping function fetchSequentially(urls, data = []) { if (urls.length === 0) return data const [url, ...rest] = urls return fetch(url) .then(res => res.text()) .then(text => fetchSequentially( rest, [...data, text] )); }

Slide 63

Slide 63 text

RUNNING PROMISES SEQUENTIALLY RUNNING PROMISES SEQUENTIALLY using await + a loop? async function fetchSequentially(urls) { const data = [] for (const url of urls) { const res = await fetch(url); const text = await res.text(); data.push(text) } return data }

Slide 64

Slide 64 text

PASSING DATA IN SEQUENTIAL ASYNC PASSING DATA IN SEQUENTIAL ASYNC CALLS CALLS return array + destructuring in next call, very verbose in Promise chains async function findLinks() { /* some implementation */ } function crawl(url, parentText) { console.log('crawling links in: ', parentText); return fetch(url) .then(res => res.text()) .then(text => Promise.all([ findLinks(text), text ])) .then(([links, text]) => Promise.all( links.map(link => crawl(link, text)) )); }

Slide 65

Slide 65 text

PASSING DATA IN SEQUENTIAL ASYNC PASSING DATA IN SEQUENTIAL ASYNC CALLS CALLS await + data in the closure async function findLinks() { /* someimplementation */ } async function crawl(url, parentText) { console.log('crawling links in: ', parentText); const res = await fetch(url); const text = await res.text(); const links = await findLinks(text); return crawl(links, text); }

Slide 66

Slide 66 text

ERROR HANDLING ❌ ERROR HANDLING ❌ try/catch, or .catch ♀ function withCatch() { return fetch('borked_url') .then(res => res.text()) .catch(err => console.log(err)) } async function withBlock() { try { const res = await fetch('borked_url'); const text = await res.text(); } catch (err) { console.log(err) } }

Slide 67

Slide 67 text

WORKSHOP WORKSHOP "callbackify"-ing a Promise-based API getting data in parallel using callbacks "promisify"-ing a callback-based API (read/write file) Why we don't mix async and sync operations

Slide 68

Slide 68 text

FURTHER READING FURTHER READING Slides/write up (including workshop examples) at About non-blocking I/O in Node.js docs: - Tyler McGinnis codewithhugo.com/async-js nodejs.org/en/docs/guides/blocking-vs-non- blocking/ Async JavaScript: From Callbacks, to Promises, to Async/Await