JavaScript North West Meetup

keith.henry@evolutionjobs.co.uk

Poll:

Have you used...

  • async/await in C#?
  • yield return in C#?
  • Promise in JS?
  • function* generators in JS?
  • Typescript or Babel to transpile JS?
  • Node.js?
  • event callbacks in JS?

Multithreading in JS

JS is not multi-threaded (sort of, a lot of browser optimisations actually are.)
Your code executes on a single thread.
(Unless you're using a Worker, but that's for another time.)
In asynchronous JS we're dealing with things that happen outside of JS, coming back onto that main thread.

Callback Hell

AKA: the problem.

Event Callbacks

The standard way of dealing with any JS event

// Fire doSomething() when the user clicks the element
element.addEventListener('click', doSomething);

Here the asynchronous thing is the user.

Thanks to JS being single threaded (sort of) doSomething() is going to finish before any other user initiated event can fire.

So far, so good. Almost all JS developers will have used this pattern.

Branches & Chains

A lot of events aren't due to user input (or maybe they just start with user input)

Often we want to do on asynchronous action after completing another - a chain.

Or we want to apply logic: if asynchronous check is true then do something different - a branch.

Error handling is essentially another branch.

Example: Overview

AJAX/HTTP-request collection of items from server

Cache results locally in IndexedDB

Example: Flow

  1. HTTP request a collection of record IDs.
    1. JSON parse the raw response into an array. callback from 1
    2. For each ID:
      1. Check IndexedDB for a local copy.
      2. Found locally: continue loop 1.2 callback from 1.2.1
      3. Not found: HTTP request the data. callback from 1.2.1
        1. JSON parse the raw response callback from 1.2.3
        2. Add the item to IndexedDB
        3. Continue loop 1.2 callback from 1.2.3.2
        4. Error from 1.2.3 - failed HTTP request
        5. Error from 1.2.3.2 - failed IndexedDB put
      4. Error 3.3 - failed IndexedDB get
    3. Error from 1 - failed HTTP request
  2. Sort the results callback from 1.2 loop completed
  3. Do something with the sorted results.
  4. Error from 3 - failed final action.

Example: Conclusion

You can see code that does this at https://www.evolutionjobs.com/uk/media/async-programming-in-js-76719

This nested soup of callbacks is called: Callback Hell

  • Changing control flow is hard.
  • Exception handling is easy to miss.
  • Decoupled or modular code is hard to write when result actions need to be passed right along a callback chain.

Promise

An object that represents an asynchronous action.

Terminology

Promises have been around as an idea for a long time, and have a lot of similar concepts:

  • thenable - anything with a .then() continuation.
  • future - in a couple of implementations future is a cut down promise.
  • deferred - jQuery's implementation.
  • task - in .NET

Promise is the New API Standard

Most promises will be returned by APIs - you'll be consuming them.

All new APIs use them.

Making a Promise: Example

// Use a promise where you would have a callback
function doThingPromise(input) {
    // Init a promise with a function that has two function parameters
    return new Promise(function (resolve, reject) {
        // Wrap a callback
		doThingCallback(input, function (result) {
			resolve(result);
		}, function (err) {
			reject(err);
		} )
    });
}

Making a Promise: Golden Rule

When you make a promise, you always follow through.

Just like real life!

Making a Promise

This means you always resolve or reject the Promise.

If there is a path through the Promise that doesn't resolve or reject then that's a fire-n-forget bug.

Keep promises small and low level.

Callback API to Promise

Let's look at 3 implementations:

ajaxPromise: XMLHttpRequest wrapper

chrome-extension-async: Chrome's extension API

idb: IndexedDB

Consuming a Promise

.then([onResolved], [onRejected]) fires when the Promise completes.

If resolve then onResolved is called.

If reject then onRejected is called.

If neither then .then() never fires!

Consuming a Promise: Chains

.then() also returns a Promise

onResolved can be:

  • undefined - onRejected is still checked.
  • A function that returns a value - continue with that value.
    // JSON parse the promise result and then log it
    doThingPromise(input).then(JSON.parse).then(console.log);
  • A function that returns another Promise - continue the chain:
    // Fetch a URL and pass the result to our Promise
    fetch(url).then(doThingPromise).then(JSON.parse).then(console.log);

The Power of Promise

Chains are a much better way of dealing with sequences of callbacks.

Exceptions are also chained, and can be easily intercepted or caught right at the end

// logException gets called if any of the chain rejects
fetch(url).then(doThingPromise).then(JSON.parse).catch(logException);

Promise is not always better

Promises work best where there is a single asynchronous thing to wait for.

Promises only resolve once.

Ask a question, wait, get an answer.

Pitfall: Promise.race()

What wins?

// Race
var tortoise = slowPromise();
var hare = fastPromise();
var dnf = rejectPromise();
var whoWins = Promise.race(tortoise, hare, dnf);

It depends on whether dnf is quicker than hare.

Think of it like motorsport: if anyone crashes there's a red flag and the race stops.

Generators

function* that returns multiple values with yield

Generators are syntactic sugar - they generate a state machine so you don't have to.

Iterator

Generators can be consumed as iterators:

// Out only generator function
function* getThings(start) {
	yield start * 2;
	yield start * 3;
	return start * 4;
}

Pull consumed with:

let generator = getThings(1);
let item = generator.next();
while(!item.done) {
	console.log(item.value);
	item = generator.next();
}

Iterator Syntax

// For-of
for(let x of getThings(1))
    console.log(x);
// Spread operator
const arrayOfAll = [...getThings(1)];
// Access the iterator function
let myObj = {};
myObj[Symbol.iterator] = function* () { /* custom iterator */ };

let nativeIterator = getThings(1)[Symbol.iterator];
// Early examples may refer to this as @@iterator, now deprecated.

Push

Generators can also have values pushed to them with yield also passing back a value:

// In/out generator function
function* keepGettingThings(start) {
	let keepGoing = true;
	while (keepGoing)
		// After the code yielded to has called .next
		keepGoing = yield ++start;

	return start;
}

Push consumed with:

let generator = getThings(3);
let item = generator.next(true);
while (!item.done) {
	console.log(item.value);

    // Pass back true until we get to 10
	item = generator.next(item.value < 10);
}

Generators are Synchronous

This isn't asynchronous code, we're just passing the active thread back and forth.

async & await

The future.

Except for Node.js, TypeScript, Babel, Chrome and (most recently) FireFox, where it is already here.

async/await vs Promise

async/await is syntactic sugar for a Promise.

async says: This function will return a Promise.

await says: return a new Promise here, and put everything after this in a .then().

Comparison: Promise

// With async/await
async function doSomethingAsync(input) {
	const url = BuildUrl(input);
	const response = await fetch(url);
	if(!response.ok)
		throw new Error(response.statusText);

	return response.json();
}
// Without
function doSomethingPromised(input) {
	var url = BuildUrl(input);
	return fetch(url).then(function(response) {
		if(!response.ok)
			throw new Error(response.statusText);

		return response.json();
	});
}

Almost! You can think of it this way, but it's using a generator.

Comparison 2: Generators

// With async/await
async function doSomethingAsync(input) {
	const url = BuildUrl(input);
	const response = await fetch(url);
	if(!response.ok)
		throw new Error(response.statusText);

	return await response.json();
}
// With generator, looks similar
function* doSomethingGenerated(input) {
	var url = BuildUrl(input);
	var response = yield fetch(url);
	if(!response.ok)
		throw new Error(response.statusText);

	return response.json();
}
// Pseudo-code consumer for generator
var generator = doSomethingGenerated(input);
var promiseResponse = generator.next().value;
promiseResponse.then(function(result) {
		var promiseJson = generator.next(result).value;
		promiseJson.then(function(result) {
			// Resolve the outer promise
			resolve(result);
		},
		generator.error)
	},
	generator.error);

Extra Credit

Not supported yet, but what should this return?

async function* both() { ...
Synchronous Asynchronous
Singular function Promise/async function
Plural Iterator function* Observable async function*

Asynchronous iterators and/or/vs Observable are rival implementations in what will become ES2018, maybe.

See also Reactive Extensions RxJS

'Rules' of async

Once you go async you never go back - functions that call async code tend to need to be async too.

When a function calls an async then it can:

  • Fire-and-forget (make a request with no callback).
  • Be async too and call the nested function with await.
  • Pass the returned Promise on up to the caller (in which case the caller now needs to be async too).

Using try-catch

await causes any chained exception to be caught:

async function getThing() {
	try {
    	const thing = await ajaxThing();
		if(thing.foo)
			thing.bar = await ajaxOtherThing();

        // Without an await this call is fire-n-forget,
		// uncomment the await to include it in the try-catch
		/*await*/ saveInLocalCache(thing);
		return thing;
	}
	catch(err) {
		// Any error thrown by any await
	}
}

Using async in Synchronous Code

If you don't await an async function then you get a Promise:

async function getThing() {
    const thing = await ajaxThing();
    return thing;
}

// Then
let t = await getThing(); // t will be the thing, once getThing() has finished

let p = getThing(); // p will immediately be a promise, that resolves with the
                    // thing once getThing() has finished

// Either of these will get us the thing
p.then(t => /* We have t */);
t = await p;

Pitfall: loops

What does this do?

// Await loop
const result = [];
for(let id of idArray)
	result.push(await getThing(id));
return result;

Next doesn't start until previous has resolved.

// Fix with await at end
for(let id of idArray)
	result.push(getThing(id));
return await Promise.all(result);

Start all the promises in parallel, then await once at the end.

JS vs .NET

.NET has supported async/await since C#5.

Similar Concepts

Task<T> instead of Promise

yield return instead of yield

(Though iterators work quite differently in .NET)

JS Cannot Block

.NET can block: wait synchronously for an asynchronous operation.

// Call Result to stop and wait for the task to complete 
var task = GetSomethingAsync();
var block = task.Result;

However, in .NET this is almost always a bad idea.

It's bad for performance and prone to thread deadlock bugs, expect to need task.ConfigureAwait(false) lots.

Other Languages

async/await support is coming in other languages too.

Python

3.2 added generators with yield from.

3.2 also added futures, which are similar to Promise or Task

3.5 brought full support for async/await.

PHP?

PHP 5.5 supports generators and yield.

The PHP Asynchronous Group has written a promise implementation.

PHP is a long way from supporting async/await.

Java?

Java has a lot of good libraries and is very powerful, but it's extremely slow to develop new language features (typically 5-10 years behind C#).

Java 8 has CompletableFuture, which is similar to Promise or Task.

No support for generators or yield.

async/await support is a very long way away.

Conclusion

Callbacks are Painful

Promise is Better

async/await Makes Using Promise Easy

Conclusion

Callbacks are painful

Promise is better

async/await makes using Promise easy

Questions?

keith.henry@evolutionjobs.co.uk
https://keithhenry.github.io/jsmeetup-async-await