kota's memex

Both node and browsers make long running operations asynchronous, rather than relying on threads. Since programming with threads is quick tricky, this is generally a good thing.

callbacks

One approach to async programming is to make functions that perform a slow action take an extra argument, a callback function. The action is started, and when it finishes, the callback function is called with the result.
setTimeout(() => console.log("Tick"), 500)

Any function that calls a function that works asynchronously must itself be asynchronous, using a callback or similar mechanism to deliver the result. Calling a callback is somewhat more involved and error-prone than simply returning a value, so needing to structure large parts of you program this way is not ideal.

promises

A promise is an asynchronous action that may complete at some point and produce a value. It is able to notify anyone who is interested when its value is available.

The easiest way to create a promise is by calling Promise.resolve. This function ensures that the value you give it is wrapped in a promise. If it's already a promise, it is simply returned; otherwise, you get a new promise that immediately finishes with your value as it's result.

let fifteen = Promise.resolve(15)
fifteen.then(value => console.log(`Got ${value}`))
// Got 15

.then

To get the result of a promise, you can use its then method. This registers a callback function to run when the promise resolves and produces a value. You can add multiple callbacks to a single promise and they will be called even if they were added after the promise resolved (finished).

That's not all the .then method does. It also returns another promise, which resolves to the value that the handler function returns or, if that returns a promise, waits for that promise and then resolves to its result.

It's useful to think of promises as a device to move values into an asynchronous reality. A normal value simply exists. A promised value is a value that might already exist or might appear at some point in the future. Computations defined in terms of promises act on such wrapped values and are executed asynchronously as the values become available.

errors

One of the most pressing problems with the callback style of asynchronous programming is that it makes it extremely difficult to properly report failures to the callbacks.

A widely used convention is that the first argument to the callback is used to indicate that the action failed, and the second contains the value produced by the action when it was successful. Such callback functions must always check whether they received an exception and make sure that any problems they cause, including exceptions thrown by the functions they call, are caught and given to the right function.

Promises make this somewhat easier. They can either be resolved (the action finished successfully) or rejected (it failed). Resolve handlers (registered with then) are called only when the action is successful, and rejections are automatically propagated to the new promise that is returned by then. When a handler throws an exception, this automatically causes the promise produced by its then call to be rejected. So if any element in a chain of asynchronous actions fails, the outcome of the whole chain is marked as rejected and no success handlers are called beyond the point where it failed.

Much like resolving a promise produces a value, rejecting one also provides one, usually called the reason of the rejection. When an exception in a handler function causes the rejection, the exception value is used as the reason. Similarly, when a handler returns a promise that is rejected, that rejection flows into the next promise. There's a Promise.reject function that creates a new, immediately rejected promise.

To explicitly handle such rejections, promises have a catch method that registers a handler to be called when the promise is rejected, similar to then. Also like then it returns a new promise, which resolves to the original promise's value is it resolves normally and to the result of the catch handler otherwise. If a catch handler throws an error, the new promise is also rejected.

As a shorthand, then also accepts a rejection handler as a second argument so you can install both types of handlers in a single method call.

The chains of promise values created by calls to then and catch can be seen as a pipeline through which asynchronous values or failures move. Since such chains are created by registering handlers, each link has a success handler or a rejection handler (or both) associated with it. Handlers that do not match the type of outcome are ignored. But those that do match are called, and their outcome determines what kind of value comes next; success when it returns a non-promise value, rejection when it throws an exception, and the outcome of a promise when it returns one of those.

new Promise((_, reject) => reject(new Error("Fail")))
	.then((value) => console.log("Handler 1"))
	.catch((reason) => {
		console.log("Caught failure " + reason)
		return "nothing"
	})
	.then((value) => console.log("Handler 2", value))
// Caught failure Error: Fail
// Handler 2 nothing

Much like an uncaught exceptions is handled by the environment, js environments can detect when a promise rejection isn't handled and will report this as an error.

collections of promises

The Promise.all function waits for all of the promises in an array to resolve and then resolves to an array of the values that these promises produced (in the same order as the original array). If any promise is rejected, the result of Promise.all is itself rejected. For example if you needed to ping a list of computers:

function availableNeighbors(computer) {
	let requests = computer.neighbors.map(neighbor => {
		return request(computer, neighbor, "ping")
		.then(() => true, () => false);
	})
	return Promise.all(requests).then(result => {
		return computer.neighbors.filter((_, i) => result[i])
	})
}

This function attaches handlers to return true or false if the ping succeeds or fails rather than allowing the whole combined promise to fail. Then in the combined promise, .filter is used to remove these from the result.

async and await

Promises are a major improvement over simple callbacks, but still in many cases result in very awkward code. An async function is a function that implicitely returns a promise and that can, in its body, await other promises such that it "looks" synchronous.

An async function is marked by the keyword async before the function keyword. Methods can also be made async and when such functions or methods are called they return a promise which is resolved (or rejected) as soon as the body returns something. The await keyword can be placed in front of an expression to wait for a promise to resolve and only then continue the execution of the function.

generators

The ability of functions to be paused and then resumed again is not exclusive to async functions. There's a feature called generator functions which is similar, but without promises.

When you define a function with function*, it becomes a generator. When you call a generator it returns an iterator.

function* powers(n) {
	for (let current = n;; current *= n) {
		yield current
	}
}

for (let power of powers(3)) {
	if (power > 50) break
	console.log(power)
}
// 3
// 9
// 27

Initially, when you call powers, the function is frozen at it's start. Every time you call next on the iterator, the function runs until it hits a yield expression, which pauses it and causes the yielded value to become the next value produced by the iterator. When the function returns (the one above never does), the iterator is done.

Generators can make creating iterators much easier. There's no need to create an object to hold the iteration state as generators automatically save their local state every time they yield.

An async function is a special type of generator. It produces a promise when called, which is resolved when it returns and rejected when it throws an exception.

the event loop

Asynchronous programs are executed piece by piece. Each piece may start some actions and schedule some code to be executed when the action finishes or fails. In between these pieces, the program sits idle, waiting for the next action.

So callbacks are not directly called by the code that scheduled them. If I call setTimeout from within a function, that function will have returned by the time the callback function is called. And when the callback returns, control does not go back to the function that scheduled it.

Asynchronous behavior happens on its own empty function call stack. This is one of the reasons that, without promises, managing exceptions across asynchronous code is hard. Since each callback starts with a mostly empty stack, you catch handlers won't be on the stack when they throw an exception:

try {
	setTimeout(() => {
		throw new Error("Woosh")
	}, 20)
} catch (_) {
	// This code will not run
	console.log("Caught")
}

No matter how closely together events such as timeouts or incoming requests happen, a javascript environment will run only one program at a time. You can think of this as it running a big loop around your program called the "event loop". When there's nothing left to be done the loop stops. As events come in, they're added to a queue, and their code is executed one after the other. Since no two things run at the same time, slow-running code might delay they handling of other events. Here's an example that sets a timeout, but then dallies until after the timeout's intended point of time, causing the timeout to be late.

let start = Date.now()
setTimeout(() => {
	console.log("Timeout ran at", Date.now() - start)
}, 20)
while (Date.now() < start + 50) {}
console.log("Wasted time until", Date.now() - start)
// Wasted time until 50
// Timeout ran at 52

Promises always resolve or reject as a new event. Even if a promise is already resolved, waiting for it will cause your callback to run after the script finishes rather than right away.

Promise.resolve("Done").then(console.log)
console.log("Me first!")
// Me first!
// Done