Learn JavaScript promises in about 70 minutes

Because twenty-five other explanations weren't enough, here I will explain JavaScript promises. This tutorial is intended for people who already understand JavaScript.

Previous educational essays of mine are Perl in 2 hours 30 minutes and regular expressions in 55 minutes.

Contents

The problem

JavaScript is single-threaded. It will only ever do at most one thing at once. There is no concurrency.

For example, if x is a global variable of some kind, then it is impossible for the value of x to change between these two lines of code:

console.log(x)
console.log(x)

This is because there are no other threads which could modify x while these two lines are executing.

*

JavaScript is event-based. It maintains a queue of messages. "A function is associated with each message. When the [call] stack is empty, a message is taken out of the queue and processed. The processing consists of calling the associated function (and thus creating an initial stack frame). The message processing ends when the stack becomes empty again."

We do not have the ability to inspect the queue, reorder it or remove messages from it.

Typically in JavaScript a message takes (or should take) an extremely small amount of time to process, of the order of milliseconds. This ensures that the application continues to appear responsive. If a message takes a very long time to process, or forever:

while(true) { }

then no other messages can be processed, and the application becomes unresponsive. Commonly this leaves us with no choice but to kill the application entirely (e.g. by closing the browser tab).

*

Now suppose we want to make a HTTP request. What we would like to write is something like this:

// BAD CODE DO NOT USE
const xhr = new XMLHttpRequest()
xhr.open("GET", "some/resource.json", false)
xhr.send()

console.log(xhr.responseText)

This is how we make a synchronous HTTP request. The call to xhr.send() blocks until the HTTP request is completed, after which we are free to inspect the XMLHttpRequest object to see the response.

But if we do this, then every other message gets blocked up behind this HTTP request. If the server takes a noticeable amount of time to respond, then the application stutters. If the server never responds, then the application locks up forever. Code like this is strongly discouraged due to the negative effect it has on the user experience.

Instead, we must write code asynchronously.

// GOOD CODE USE THIS INSTEAD
const xhr = new XMLHttpRequest()

xhr.addEventListener("load", () => {
	console.log(this.responseText)
})

xhr.open("GET", "some/resource.json")
xhr.send()

Here, the xhr.send() call returns immediately, and JavaScript continues working, while carrying out the real HTTP request in the background (i.e. on a thread to which we, the JavaScript programmer, do not have programmatic access). This is called non-blocking I/O.

Later, when (if) the HTTP request is completed, a new message will be placed in the message queue. The message will be associated with that listener function:

() => {
	console.log(this.responseText)
}

and, when JavaScript eventually reaches that message in the queue, the function will be called.

*

Now, how can we turn this HTTP request-making code into a callable function? What we want to write in our calling code is something like:

const responseText = get("some/resource.json")

But neither of these approaches works:

const get = resource => {
	const xhr = new XMLHttpRequest()
	xhr.addEventListener("load", () => {
		return this.responseText // this value goes nowhere
	})
	xhr.open("GET", resource)
	xhr.send()
	return xhr.responseText // returns `undefined`
}

The solution is callbacks. When calling get, we pass in our URL but also a callback function. When — at some nebulous future time — get has finished its task, it will call that function, passing the result in.

const get = (resource, callback) => {
	const xhr = new XMLHttpRequest()
	xhr.addEventListener("load", () => {
		callback(this.responseText)
	})
	xhr.open("GET", resource)
	xhr.send()
}

Usage:

get("some/resource.json", responseText => {
	console.log(responseText)
})

This is called continuation-passing style. As a general pattern, this works well, but it is not very pretty. It means that all later code has to be placed inside, or called from, that callback.

*

Interestingly, this pattern can even find use in functions which would not normally be asynchronous. A function like:

const parseJson = json => {
	const obj = JSON.parse(json)
	return obj
}

becomes:

const parseJson = (json, callback) => {
	setTimeout(() => {
		const obj = JSON.parse(json)
		callback(obj)
	}, 0)
}

Here, setTimeout() is simply putting a message on the queue right away, without any delay. Although the interval specified is 0 milliseconds, this does not mean that

() => {
	const obj = JSON.parse(json)
	callback(obj)
}

will be invoked immediately; other messages which have been queued up in the meantime will be handled first. We might do this if we are carrying out a long single calculation and we want to break it up into smaller messages.

Note that parseJson no longer explicitly returns anything, which is another way of saying that it returns undefined. The same is true of get and any other function employing this pattern.

*

This pattern of callbacks becomes troublesome when we need to carry out several tasks in sequence:

get("some/resource.json", responseText => {
	parseJson(responseText, obj => {
		extractFnord(obj, fnord => {
			console.log(fnord)
		})
	})
})

Note the increasingly severe indentation. This is called callback hell.

The worse problem

How do we handle errors in this scenario? There are several ways to do this, but they both tend to make the situation even less readable.

Asynchronous error handling technique 1

One technique for handling errors is this:

const get = (resource, callback) => {
	const xhr = new XMLHttpRequest()
	xhr.addEventListener("load", () => {
		if(this.status === 200) {
			callback(undefined, this.responseText)
		} else {
			callback(Error())
		}
	})
	xhr.addEventListener("error", () => {
		callback(Error())
	})
	xhr.open("GET", resource)
	xhr.send()
}

And:

const parseJson = (json, callback) => {
	setTimeout(() => {
		try {
			const obj = JSON.parse(json)
			callback(undefined, obj)
		} catch(e) {
			callback(e)
		}
	}, 0)
}

Here we pass two arguments to the callback, the first of which is an error. Customarily, if nothing goes wrong, then err is falsy.

Usage:

get("some/resource.json", (err1, responseText) => {
	if(err1) {
		console.error(err1)
		return
	}
	parseJson(responseText, (err2, obj) => {
		if(err2) {
			console.error(err2)
			return
		}
		extractFnord(obj, (err3, fnord) => {
			if(err3) {
				console.error(err3)
				return
			}
			console.log(fnord)
		})
	})
})

This approach is seen very commonly. As a prime example, take a look at the fs module in Node.js, which carries out filesystem operations, another form of I/O. Compare this blocking I/O function:

const str = fs.readFileSync("example.txt", "utf8")
console.log(str)

with this non-blocking version:

fs.readFile("example.txt", "utf8", (err, str) => {
	if(err) {
		console.error(err)
		return
	}
	console.log(str)
})

Asynchronous error handling technique 2

The other way to handle errors is to pass two callbacks into the calculation. One is for success, and the other is for failure:

const get = (resource, callback, errback) => {
	const xhr = new XMLHttpRequest()
	xhr.addEventListener("load", () => {
		if(this.status === 200) {
			callback(this.responseText)
		} else {
			errback(Error())
		}
	})
	xhr.addEventListener("error", () => {
		errback(Error())
	})
	xhr.open("GET", resource)
	xhr.send()
}

And:

const parseJson = (json, callback, errback) => {
	setTimeout(() => {
		try {
			const obj = JSON.parse(json)
			callback(obj)
		} catch(e) {
			errback(e)
		}
	}, 0)
}

Usage:

get("some/resource.json", responseText => {
	parseJson(responseText, obj => {
		extractFnord(obj, fnord => {
			console.log(fnord)
		}, err3 => {
			console.error(err3)
		})
	}, err2 => {
		console.error(err2)
	})
}, err1 => {
	console.error(err1)
})

This is marginally better, but still pretty ghastly. Both approaches have drawbacks. In addition to the tedious repetition, there are issues like:

  • In principle, there's nothing stopping callback from being called multiple times, likely with different parameters.
  • There's also nothing to stop errback from being called multiple times.
  • There's nothing to stop callback and errback from both being called, in either order.
  • There's no clear specification of what arguments callback and/or errback should have, how many arguments there should be, what should happen if the functions are called with the wrong parameters, or what this should be inside those functions.
  • If callback and/or errback are omitted then attempting to call them will cause a TypeError to be thrown.
  • If an exception is thrown, there's an excellent chance that neither callback nor errback will ever be called; the exception will disappear into oblivion and execution will stop entirely.
  • It's not possible to set up multiple distinct callbacks or errbacks.
  • It's not possible to add callbacks or errbacks after the fact.
  • It really looks like there ought to be some kind of chaining capability.

All of these things could be handled using sufficient amounts of boilerplate. But surely there's a better way? A standardised programming construct which solves this problem?

Introducing promises

Let's do some refactoring. Rewrite get like so:

const get = resource => {
	return new Promise((resolve, reject) => {
		const xhr = new XMLHttpRequest()
		xhr.addEventListener("load", () => {
			if(this.status === 200) {
				resolve(this.responseText)
			} else {
				reject(Error())
			}
		})
		xhr.addEventListener("error", () => {
			reject(Error())
		})
		xhr.open("GET", resource)
		xhr.send()
	})
}

And similarly parseJson:

const parseJson = json => {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			try {
				const obj = JSON.parse(json)
				resolve(obj)
			} catch(e) {
				reject(e)
			}
		}, 0)
	})
}

The asynchronous functions no longer accept callbacks, either for success or for failure. But they do return something now. The object returned is called a promise. The promise represents the task which get or parseJson (or extractFnord) has promised to do. It has three states: pending, fulfilled or rejected.

A promise starts out pending. The inner (resolve, reject) => { ... }, which we supply, is called the executor callback. The promise calls this function, passing in two arguments, resolve and reject. When the task has been done, we call resolve to fulfill the promise i.e. mark the task as completed. Or, if the task has failed, we call reject to reject the promise i.e. mark the task as failed. A promise which is fulfilled or rejected is called settled.

By interacting with this promise object, we can register success and error callbacks to be called when it settles. But first, some bullet points!

  • Once settled, a promise can never change state again.

  • If we call resolve() twice, the second call will be silently ignored.

  • If we call reject() twice, the second call will be silently ignored.

  • If we call resolve() and then call reject(), or vice-versa, the second call will be silently ignored. All three/four of these cases are probably mistakes, though.

  • If we neglect to call either resolve() or reject(), the promise will never settle. Again, this is probably a mistake.

  • A fulfilled promise has a value. Call resolve(value) to fulfill the promise with a value. The value can be anything except another promise.

  • If we try to fulfill a promise with a value which is a second promise, the first promise instead settles the same way, and with the same value or error, as the second promise. This is crucially important, but it is also a little difficult to grasp in the abstract. Concrete examples will appear in just a minute.

  • It's not possible to change the value once set.

  • A rejected promise has an error. Call reject(err) to reject the promise with an error. The error can be anything, even another promise, but is commonly an Error.

  • It's not possible to change the error once set.

  • Calling resolve() or reject() with no parameters is equivalent to calling them with a single parameter, undefined.

  • Extra parameters passed to resolve() or reject(), past the first one, will be silently ignored.

  • The value returned by the executor callback, if any, goes nowhere, and does nothing.

  • The value of this inside the executor callback is undefined — provided that we are using strict mode, which we always should be.

  • throwing a value is equivalent to calling reject() with that value, so no need for catch(e) { reject(e) }.

And also introducing then()

Now here's a very important point: there is no way to directly inspect the current state of a promise, or the value it fulfilled with (if any), or the error it rejected with (if any).

Instead, we call then(callback, errback) on a promise to register a success callback and an error callback. And so our usage now looks like this:

get("some/resource.json").then(responseText => {
	parseJson(responseText).then(obj => {
		extractFnord(obj).then(fnord => {
			console.log(fnord)
		}, err3 => {
			console.error(err3)
		})
	}, err2 => {
		console.error(err2)
	})
}, err1 => {
	console.error(err1)
})

This still isn't great, but we're gradually getting closer to something good. First, more bullet points:

  • If and when the promise (such as get("some/resource.json")) fulfills with a value, the success callback will be called with that value.

  • If and when the promise rejects with an error, the error callback will be called with that error.

  • Since a promise cannot fulfill twice, the success callback will only ever be called at most once.

  • Since a promise cannot reject twice, the error callback will only ever be called at most once.

  • Since a promise cannot fulfill and reject, for any given then(callback, errback) pair, either the success callback or the error callback will be called (or neither), never both.

  • The second argument to then() is optional; if we omit it and the promise rejects, nothing happens.

  • The first argument to then() is also optional; if we omit it (i.e. then(undefined, errback)) and the promise fulfills, nothing happens.

  • As a shorthand for then(undefined, errback), we can use catch(errback).

  • The success or error callbacks, if called, will always be passed exactly one argument.

  • The success or error callbacks, if called, will be called in a separate message, not synchronously when the promise settles.

  • The value of this inside the success and error callbacks is undefined — provided that we are using strict mode, which we always should be.

  • We don't actually have to call then(); the executor callback will still be called and whatever task was listed there will still be carried out, or at least attempted.

  • We can call then() on the same promise multiple times; when the promise settles, all the registered success or error callbacks will be called in the order in which they were registered.

  • It is totally okay to call then() on a promise after it has already been settled; the appropriate callback will be invoked right away. (Likewise, in the executor callback, it's fine to call resolve() or reject() before any success or error callbacks have been registered.) On that topic:

  • We can create a promise which has already been fulfilled, using Promise.resolve("foo").

  • We can create a promise which has already been rejected, using Promise.reject("bar").

  • then() returns a new promise. This enables us to chain promises together, setting up a sequence of asynchronous operations. This is the most important feature of promises and warrants a full section...

Promise chaining

Calling then(callback, errback) on a promise returns a new promise. The way in which the new promise settles depends on two things:

  1. The way in which the first promise settles.
  2. What happens inside the success or error callback.

Here are some examples.

  • This promise:

    Promise.resolve("foo")
    

    fulfills with value "foo".

  • But this promise:

    Promise.resolve("foo").then(str => {
    	return str + str
    })
    

    fulfills with value "foofoo". This gives us a basic ability to synchronously transform fulfilled values.

  • This promise:

    Promise.resolve("foo").then(str => {
    	throw str + str
    })
    

    rejects with error "foofoo".

  • This promise:

    Promise.reject("bar")
    

    rejects with error "bar".

  • This promise:

    Promise.reject("bar").catch(err => {
    	return err + err
    })
    

    fulfills with value "barbar". This is because the error callback is intended to be an error handler. If the error callback returns normally, then it is assumed that the error has been handled gracefully, and the returned value is the value with which to fulfill the new promise.

  • If our error callback is unable to handle an error gracefully, correct behaviour is to throw a new error. This promise:

    Promise.reject("bar").catch(err => {
    	throw err + err
    })
    

    rejects with value "barbar". Alternatively, if there's no way that this error could be handled gracefully at this time, we could simply omit the error callback entirely and allow the error to continue to propagate.

*

In a success or error callback, we can return or throw nearly any value we like. If we don't return anything, this is the same as returning undefined, so the fulfilled value of the new promise is undefined, which is fine. There's one special case, which we may remember from earlier:

If we fulfill a promise with a second promise, the first promise settles the same way as the second

This applies no matter what method we use to fulfill that first promise, be it Promise.resolve():

Promise.resolve("foo")
// fulfills with "foo"

Promise.reject("bar")
// rejects with "bar"

Promise.resolve(Promise.resolve("foo"))
// fulfills with "foo"

Promise.resolve(Promise.reject("bar"))
// rejects with "bar"

Promise.resolve(Promise.resolve(Promise.resolve("foo")))
// fulfills with "foo"

Promise.resolve(Promise.resolve(Promise.reject("bar")))
// rejects with "bar"

// and so on

Or the executor callback:

new Promise((resolve, reject) => { resolve("foo") })
// fulfills with "foo"

new Promise((resolve, reject) => { reject("bar") })
// rejects with "bar"

new Promise((resolve, reject) => { resolve(Promise.resolve("foo")) })
// fulfills with "foo"

new Promise((resolve, reject) => { resolve(Promise.reject("bar")) })
// rejects with "bar"

// and so on

Or a success or error callback:

Promise.resolve().then(() => { return "foo" })
// fulfills with "foo"

Promise.resolve().then(() => { throw "bar" })
// rejects with "bar"

Promise.resolve().then(() => { return Promise.resolve("foo") })
// fulfills with "foo"

Promise.resolve().then(() => { return Promise.reject("bar") })
// rejects with "bar"

// and so on

(Note that it's totally okay to reject a promise with a second promise:

Promise.reject(Promise.resolve("foo"))
// rejects with `Promise.resolve("foo")`

new Promise((resolve, reject) => { reject(Promise.resolve("foo")) })
// rejects with `Promise.resolve("foo")`

Promise.resolve().then(() => { throw Promise.resolve("foo") })
// rejects with `Promise.resolve("foo")`

// and so on

But this is a rather odd thing to do...)

*

Why is this so significant?

Because it means that we can asynchronously transform values as well. Which allows us to suddenly turn this code:

get("some/resource.json").then(responseText => {
	parseJson(responseText).then(obj => {
		extractFnord(obj).then(fnord => {
			console.log(fnord)
		}, err3 => {
			console.error(err3)
		})
	}, err2 => {
		console.error(err2)
	})
}, err1 => {
	console.error(err1)
})

into this:

get("some/resource.json").then(responseText => {
	return parseJson(responseText)
}).then(obj => {
	return extractFnord(obj)
}).then(fnord => {
	console.log(fnord)
}).catch(err => {
	console.error(err)
})

And boom! We're out of callback hell!

*

Let's break our new asynchronous code down. There are five promises in the main chain.

  • The first:

    get("some/resource.json")
    

    fulfills with responseText.

  • The second:

    get("some/resource.json").then(responseText => {
    	return parseJson(responseText)
    })
    

    settles the same way as parseJson(responseText) does; that is, fulfills with the parsed JSON object, obj.

  • The third:

    get("some/resource.json").then(responseText => {
    	return parseJson(responseText)
    }).then(obj => {
    	return extractFnord(obj)
    })
    

    settles the same way as extractFnord(obj) does; that is, fulfills with fnord.

  • The fourth:

    get("some/resource.json").then(responseText => {
    	return parseJson(responseText)
    }).then(obj => {
    	return extractFnord(obj)
    }).then(fnord => {
    	console.log(fnord)
    })
    

    fulfills with the value returned from that last success callback, i.e. undefined.

  • And the fifth:

    get("some/resource.json").then(responseText => {
    	return parseJson(responseText)
    }).then(obj => {
    	return extractFnord(obj)
    }).then(fnord => {
    	console.log(fnord)
    }).catch(err => {
    	console.error(err)
    })
    

    fulfills the same way as the fourth does; that is, fulfills with value undefined.

*

One more thing. Since the callback functions are guaranteed to be called with only a single argument, and the value of this passed will be undefined, and our intermediate functions parseJson and extractFnord don't use this internally, our code may be simplified even further:

get("some/resource.json")
	.then(parseJson)
	.then(extractFnord)
	.then(fnord => {
		console.log(fnord)
	}).catch(err => {
		console.error(err)
	})

In some browsers, console.log and console.error aren't sensitive to the value of this either, so we can even go as far as:

get("some/resource.json")
	.then(parseJson)
	.then(extractFnord)
	.then(console.log)
	.catch(console.error)

Amazing!

To take maximum advantage of this pattern, write functions (and methods!) which

  1. Accept only a single argument
  2. Do not use this (or, use this but also use bind() to fix its value)
  3. Return a promise
*

So here's a fun edge case. If we fulfill a promise with a second promise, the first promise settles the same way as the second. What if the second promise is the first promise?

const p = new Promise((resolve, reject) => {
	setTimeout(() => {
		resolve(p)
	}, 0)
})

(Note that setTimeout must be used here, since the executor callback is called synchronously at Promise construction time, at which time the value of p is still undefined.)

Answer: this promise rejects with a TypeError because of the cycle that has been introduced.

Let's use this to segue into the topic of error handling.

Error handling in a promise chain

If a promise rejects, execution passes to the next available error callback. To demonstrate how this works, we'll start with a basic promise chain:

Promise.resolve("foo").then(str => {
	return str + str
}).then(str => {
	console.log(str)
}, err => {
	console.error(err)
})

and see what happens if we introduce errors — which is to say, cause promises to reject — at various points.

This code:

Promise.reject("bar").then(str => {
	return str + str
}).then(str => {
	console.log(str)
}, err => {
	console.error(err)
})

hits none of the success callbacks, and immediately errors out printing "bar".

This code:

Promise.resolve("foo").then(str => {
	throw str + str
}).then(str => {
	console.log(str)
}, err => {
	console.error(err)
})

hits the first success callback, then errors out printing "foofoo". The second success callback is not hit.

And finally, this code:

Promise.resolve("foo").then(str => {
	return str + str
}).then(str => {
	throw str
}, err => {
	console.error(err)
})

prints nothing at all!

Remember: when we use then(callback, errback), either the success callback or the error callback will be called, never both. errback does not handle exceptions thrown by callback. callback and errback are both intended to handle the outcome from the previous promise, not from each other.

Because of this potential for "leaking" an error, I think it is good practice to never call then(callback, errback), passing in both callbacks. It's safer to always use then(callback), passing in only one callback, or, when handling errors at the tail end of a chain, to use catch(errback):

Promise.resolve("foo").then(str => {
	return str + str
}).then(str => {
	throw str
}).catch(err => {
	console.error(err)
})

And in general, we should always conclude a promise chain with a catch() call, because otherwise errors in the chain will disappear and never be detected.

Of course, if an exception is thrown during an error callback, then we may be out of luck entirely, but that's a standing problem with all of error handling...

Implementation variations

Throughout this document we have been using the native Promise implementation which is present in many JavaScript engines. This is a relatively new feature of JavaScript and does not have universal support; in particular, it is not available in Internet Explorer. However, there are numerous third-party implementations of promises which work in a basically identical way, such as Q and Bluebird.

All these implementations conform to a technical specification called Promises/A+. This specification only really specifies the behaviour of the then() method. The then() method will work identically in all conforming implementations.

Everything else is left up to implementers. For example, the APIs for:

  • constructing a new promise to carry out a particular task (here new Promise((resolve, reject) => {}))
  • creating pre-fulfilled promises (here Promise.resolve("foo"))
  • creating pre-rejected promises (here Promise.reject("bar"))
  • easily adding an error callback with no success callback (here catch(() => {}))

are not specified by Promises/A+. The native Promise implementation specified in ES6 works like this, and other implementations generally work similarly, but these APIs are not necessarily universal.

Different implementations are also at liberty to offer whatever additional functionality they wish. Promise offers two other methods worth mentioning, Promise.all() and Promise.race(), both of which accept an array of promises run "in parallel". Promise.all() fulfills with an array containing the fulfilled values from all the inner promises, Promise.race() fulfills with the value of the first inner promise to fulfill. Other implementations usually offer equivalent functionality and often offer much more functionality.

Conclusion

I find promises to be heck of complicated and it wasn't until I sat down and tried to write this that I realised just how poorly I understood them. Not pictured here is the lengthy interlude where I gave up and, as a learning exercise, attempted to read, understand and implement the Promises/A+ specification myself, which nearly caused my head to explode. Anyway, I think I have a pretty good handle on them now, and I hope you do too.

Discussion (22)

2016-09-10 03:27:22 by hobbs:

As someone who mostly does understand promises, I'd say that this is pretty good! I don't see any big lies or mistakes in it, but it avoids going into unnecessary and confusing depth. One big insight that helped me understand promises is that callbacks are a form of enforced inversion-of-control. Instead of writing functions that return a value, you have to write a function that *takes a callback* that accepts a value. That's what makes callback-based code unnatural. Promises, then, are un-inversion of control (or really, double-inversion of control). It's still callbacks under the hood, but you can go back to writing code that returns things.

2016-09-10 07:49:04 by jbox:

As someone who doesn't understand javascript but has been reading about monads for the past couple days, I have to say that promises sounds exactly like an application of monads. Which is pretty nice because now I know of a real use of monads outside of a purely functional language.

2016-09-11 10:15:02 by Veky:

No, sorry. That was completely impenetrable for me. But I don't know JavaScript very well.

2016-09-12 18:08:58 by qntm:

Sorry, I should have made it clearer up front that this essay was intended for people who are already familiar with JavaScript in general.

2016-09-13 18:11:59 by Jymbob:

... Then discover you still need to support IE11 and spend another 2 hours researching polyfills...

2016-09-15 16:02:00 by Baughn:

IE11 isn't so bad. I've written code that still needs to support IE6. But let's leave that aside. Building on the callback functionality that JS already gives us, another alternative is to use a transpiler that implements async/await, e.g. as seen in C#. It isn't as easy to retrofit, thus the transpiler, but in my experience it's a lot easier to explain. The transpiler needs to implement a continuation-passing transform. More relevantly, if you don't have a transpiler and you don't have promises, it's possible to use continuation-passing style manually. It's not as convenient as promises, but it's better than ad-hoc use of callbacks.

2016-09-16 10:54:39 by m1el:

I think that Promises is the best thing that happened to the JS world, and this video just clicked in my head "a-ha, this is the mathematical meaning of Promises!" https://vimeo.com/113707214 So another nice way to explain promises is to draw railways :) As a side-note, <em>implementing</em> Promise is an interesting excercise.

2016-09-16 11:06:52 by m1el:

Another thing: JS is *very* dynamic, so "impossible for the value of x to change between these two lines of code" is sort of false :) var scope = {}; scope.__defineGetter__('x', ((a=0)=>_=>a++)()); with (scope) { // two lines of code ^^ console.log(x); console.log(x); // end }

2016-09-16 11:37:57 by qntm:

Well, `x` wouldn't really be a "global variable" in that case, would it? You could achieve the same result without using the strongly deprecated `with` statement or violating strict mode by doing this: var x = 7; var console = { log: function(b) { global.console.log(b); x++; } }; console.log(x); // 7 console.log(x); // 8 But at that point it's pretty obvious that you're already a JavaScript expert...

2016-09-18 16:47:05 by Tim McCormack:

Looks pretty good. I think it took me about 30 minutes, but I have a fair amount of experience with JS, callbacks, and RxJava. Hard to say for sure because I kept getting interrupted by a baby. ---- This part confused me a bit: "We don't actually have to call then(); the executor callback will still be called and whatever task was listed there will still be carried out, or at least attempted." Is that saying that the Promise will still execute, even if we don't register callbacks for it? If so, I'm confused about the definition of "executor callback" because in that case there shouldn't be one... ---- I'm uneasy with the idea that returning a Promise from an executor callback directly sets the state of the former Promise. What happens if you have a generic asynchronous algorithm that shuttles all sorts of data around, and some caller sends in a Promise instead of an integer? I don't like in-band signalling...

2016-09-19 23:44:47 by qntm:

> Is that saying that the Promise will still execute, even if we don't register callbacks for it? If so, I'm confused about the definition of "executor callback" because in that case there shouldn't be one... The executor callback is the `function(resolve, reject) { ... }` which you can pass into the `Promise` constructor. This is not to be confused with the success callback and error callback, which you can register with any promise using `.then(function(value) { ... }, function(err) { ... })`. > I'm uneasy with the idea that returning a Promise from an executor callback directly sets the state of the former Promise. What happens if you have a generic asynchronous algorithm that shuttles all sorts of data around, and some caller sends in a Promise instead of an integer? I don't like in-band signalling... In that case you have an instance of that monolithic problem which afflicts all dynamically typed programming languages: any data can be passed into any function, so you have to either trust that the data is of the correct type, or manually check it and throw an exception if it isn't. Yes, this makes it tricky to e.g. have a promise which "sorts" a list of promises and returns the "greatest" one as its successful result. I guess you'd have to box the input promises up somehow and unbox on return. But, hopefully, that's an extremely rare edge case.

2016-09-20 04:03:11 by Alexandre:

This tutorial was well timed for me and extremely useful. Thanks for writing it! I don't know if this was your innovation or a convention that I've never noticed but highlighting the new code in red was super helpful. Tiniest nitpick, repeated use of even: "... this pattern can even find use even in functions ..."

2016-09-20 04:04:36 by Alexandre:

Oh also this took me about 60 minutes to get through. The 70 minute estimate was pretty close!

2016-09-24 00:04:55 by Dom Storey:

As always in depth and detailed. Promises make JS functional languages slightly more bearable!! :-)

2016-10-11 19:16:10 by GeorgeWL:

@Sam I'd love to hear more about the CMS you use for this some day btw. Is it custom-built or is it a plugged in thing?

2016-10-11 19:17:59 by qntm:

I wrote it myself in PHP about eight years ago. The code is pretty ghastly.

2018-01-27 18:34:38 by Marco:

var get = function(resource) { var xhr = new XMLHttpRequest(); xhr.addEventListener("load", function() { return this.responseText; // this value goes nowhere }); xhr.open("GET", resource); xhr.send(); return xhr.responseText; // returns `undefined` }; that's not true, to me it doesn't return `undefined`, it returns the value of the request

2018-01-27 18:43:00 by qntm:

What platform are you on?

2018-07-26 19:37:00 by AridWaste:

I've been using javascript for many years, but had never heard of Promises before. This guide is just about the clearest and most useful javascript-related document I have ever read.

2022-02-09 21:01:16 by Cat:

Thanks, I love this. Basically everything I need to know about Promises, all in one place.

2022-05-29 15:52:47 by tacooper:

Great tutorial. Noticed a minor typo if you care to fix: new Promise((resolve, reject) => { resolve(Promise.reject("foo")) }) // rejects with "bar"

2022-05-29 16:12:37 by qntm:

Fixed.

New comment by :

Plain text only. Line breaks become <br/>
The square root of minus one: