Thenable Duck Typing

In Promises-land, an important detail is how to know for sure if some value is a genuine Promise or not. Or more directly, is it a value that will behave like a Promise?

Given that Promises are constructed by the new Promise(..) syntax, you might think that p instanceof Promise would be an acceptable check. But unfortunately, there are a number of reasons that’s not totally sufficient.

Mainly, you can receive a Promise value from another browser window (iframe, etc.), which would have its own Promise different from the one in the current window/frame, and that check would fail to identify the Promise instance.

Moreover, a library or framework may choose to vend its own Promises and not use the native ES6 Promise implementation to do so. In fact, you may very well be using Promises with libraries in older browsers that have no Promise at all.

When we discuss Promise resolution processes later in this chapter, it will become more obvious why a non-genuine-but-Promise-like value would still be very important to be able to recognize and assimilate. But for now, just take my word for it that it’s a critical piece of the puzzle.

As such, it was decided that the way to recognize a Promise (or something that behaves like a Promise) would be to define something called a “thenable” as any object or function which has a then(..) method on it. It is assumed that any such value is a Promise-conforming thenable.

The general term for “type checks” that make assumptions about a value’s “type” based on its shape (what properties are present) is called “duck typing” — “If it looks like a duck, and quacks like a duck, it must be a duck” (see the Types & Grammar title of this book series). So the duck typing check for a thenable would roughly be:

  1. if (
  2. p !== null &&
  3. (
  4. typeof p === "object" ||
  5. typeof p === "function"
  6. ) &&
  7. typeof p.then === "function"
  8. ) {
  9. // assume it's a thenable!
  10. }
  11. else {
  12. // not a thenable
  13. }

Yuck! Setting aside the fact that this logic is a bit ugly to implement in various places, there’s something deeper and more troubling going on.

If you try to fulfill a Promise with any object/function value that happens to have a then(..) function on it, but you weren’t intending it to be treated as a Promise/thenable, you’re out of luck, because it will automatically be recognized as thenable and treated with special rules (see later in the chapter).

This is even true if you didn’t realize the value has a then(..) on it. For example:

  1. var o = { then: function(){} };
  2. // make `v` be `[[Prototype]]`-linked to `o`
  3. var v = Object.create( o );
  4. v.someStuff = "cool";
  5. v.otherStuff = "not so cool";
  6. v.hasOwnProperty( "then" ); // false

v doesn’t look like a Promise or thenable at all. It’s just a plain object with some properties on it. You’re probably just intending to send that value around like any other object.

But unknown to you, v is also [[Prototype]]-linked (see the this & Object Prototypes title of this book series) to another object o, which happens to have a then(..) on it. So the thenable duck typing checks will think and assume v is a thenable. Uh oh.

It doesn’t even need to be something as directly intentional as that:

  1. Object.prototype.then = function(){};
  2. Array.prototype.then = function(){};
  3. var v1 = { hello: "world" };
  4. var v2 = [ "Hello", "World" ];

Both v1 and v2 will be assumed to be thenables. You can’t control or predict if any other code accidentally or maliciously adds then(..) to Object.prototype, Array.prototype, or any of the other native prototypes. And if what’s specified is a function that doesn’t call either of its parameters as callbacks, then any Promise resolved with such a value will just silently hang forever! Crazy.

Sound implausible or unlikely? Perhaps.

But keep in mind that there were several well-known non-Promise libraries preexisting in the community prior to ES6 that happened to already have a method on them called then(..). Some of those libraries chose to rename their own methods to avoid collision (that sucks!). Others have simply been relegated to the unfortunate status of “incompatible with Promise-based coding” in reward for their inability to change to get out of the way.

The standards decision to hijack the previously nonreserved — and completely general-purpose sounding — then property name means that no value (or any of its delegates), either past, present, or future, can have a then(..) function present, either on purpose or by accident, or that value will be confused for a thenable in Promises systems, which will probably create bugs that are really hard to track down.

Warning: I do not like how we ended up with duck typing of thenables for Promise recognition. There were other options, such as “branding” or even “anti-branding”; what we got seems like a worst-case compromise. But it’s not all doom and gloom. Thenable duck typing can be helpful, as we’ll see later. Just beware that thenable duck typing can be hazardous if it incorrectly identifies something as a Promise that isn’t.