TIL: Promise resolution with objects including a then property
Tags: javascript
Published: 2019-03-04 01:17:39

---
title: TIL: Promise resolution with objects including a then property
published: true
tags: javascript
canonical_url: https://www.stefanjudis.com/today-i-learned/promise-resolution-with-objects-including-a-then-property/
---

**tl;dr**

*When you resolve a promise with an object that defines a `then` method "standard promise behavior" takes place. The `then` method will be executed with `resolve` and `reject` arguments immediately. Calling `then` with other values overwrites the initial promise resolution value. This behavior enables recursive promise chains.*

*The reasonably new `import` method to load JavaScript modules is no exception to that.*

---

Recently, two tweets covering promises and dynamic imports caught my attention. I spent two hours reading the spec, and this post shares my thought process and what I learned about promises and promise chains.

### Tweet 1: A way to "kinda" hack together top-level await

Surma shared ["a hack to make top-level await work"](https://twitter.com/DasSurma/status/1101461224317956101).

You can include an inline script of `type="module"` in your HTML which dynamically imports another module.

```html
<script type="module">
  import('./file.mjs');
</script>
```

The module itself exports a `then` function which will be executed immediately without anything calling it.

```javascript
// file.mjs
export async function then() {
  // yay!!!      I can use async/await here
  // also yay!!! this function will be executed automatically
}
```

You could use this behavior to define `file.mjs` as the entry point of your application and use async/await right await in the `then` function. 

**Important detail: the `then` function is executed automatically.**

### Tweet 2: The blocking behavior of dynamic imports

[Johannes Ewald shared](https://twitter.com/Jhnnns/status/1100074123118735360) that dynamic imports can "block" code execution when the returned value of the import includes a `then` function.

```javascript
// file.mjs
export function then() {}

// index.mjs
async function start() {
  const a = await import('./file.mjs');
  // the following lines will never be executed
  console.log(a);
}
``` 

The snippets above will never log anything.

**Important detail: `import('./file.mjs')` never resolves.**

## The promise resolution process

The behavior you saw in the examples above is not related to the `import` spec ([a GitHub issue](https://github.com/tc39/proposal-dynamic-import/issues/47) describes this behavior in great detail). [The ECMAscript spec](https://tc39.github.io/ecma262/#sec-promise-resolve-functions) describing the resolution process of promises is the foundation instead.

```
8.  If Type(resolution) is not Object, then
      a. Return FulfillPromise(promise, resolution).

9.  Let then be Get(resolution, "then").

10. If then is an abrupt completion, then
      a. Return RejectPromise(promise, then.[[Value]]).

11. Let thenAction be then.[[Value]].

12. If IsCallable(thenAction) is false, then
      a. Return FulfillPromise(promise, resolution).
     
13. Perform EnqueueJob(
      "PromiseJobs", PromiseResolveThenableJob, « promise, resolution, thenAction »
    ).
```

Let's go over the possibilities to resolve a promise step by step.

### Promise resolves with anything else than an object

> If Type(resolution) is not Object, then return FulfillPromise(promise, resolution)

If you resolve a promise with a string value (or anything that is not an object), this value will be the promise resolution.

```javascript
Promise.resolve('Hello').then(
  value => console.log(`Resolution with: ${value}`)
);

// log: Resolution with: Hello
```

### Promise resolves with an object including `then` which is an `abruptCompletion`

> Let then be Get(resolution, "then"). If then is an abrupt completion, then return RejectPromise(promise, then.[[Value]]).

If you resolve a promise with an object including a `then` property which's access results in an exception, it leads to a rejected promise.

```javascript
const value = {};
Object.defineProperty(
  value,
  'then',
  { get() { throw new Error('no then!'); } }
);

Promise.resolve(value).catch(
  e => console.log(`Error: ${e}`)
);

// log: Error: no then!
```

### Promise resolves with an object including `then` which is not a function

>  Let thenAction be then.[[Value]]. If IsCallable(thenAction) is false, then return FulfillPromise(promise, resolution).

If you resolve a promise with an object including a `then` property which is not a function, the promise is resolved with the object itself.

```javascript
Promise.resolve(
  { then: 42 }
).then(
  value => console.log(`Resolution with: ${JSON.stringify(value)}`)
);

// log: Resolution with: {"then":42}
``` 

### Promise resolves with an object including `then` which is a function

Now, we come to the exciting part which is the foundation for recursive promise chains. I started going down [the rabbit hole](https://tc39.github.io/ecma262/#sec-promiseresolvethenablejob) to describe the complete functionality, but it would include  references to several other parts of the ECMAScript spec. Going into the details would be out of scope for this post.

The critical part of this last step is that when a promise resolves with an object that includes a `then` method the resolution process will call `then` with the usual promise arguments `resolve` and `reject` to evaluate the final resolution value. If `resolve` is not called the promise will not be resolved.

```javascript
Promise.resolve(
  { then: (...args) => console.log(args) }
).then(value => console.log(`Resolution with: ${value}`));

// log: [fn, fn]
//        |   \--- reject
//     resolve

// !!! No log of a resolution value
```

This defined behavior leads to the forever pending promise of the second Tweet example. `resolve` is not called and thus the promise never resolves.

```javascript
Promise.resolve(
  { 
    then: (resolve) => { 
      console.log('Hello from then');
      resolve(42);
    }
  }
).then(value => console.log(`Resolution with: ${value}`));

// log: Hello from then
// log: Resolution with: 42
```

## It all ties together

Luckily the behavior shared on Twitter now makes sense to me. Additionally, it's the described behavior that you use to chain promise recursively every day.

```javascript
(async () => {
  const value = await new Promise((resolve, reject) => {
    // the outer promise will be resolved with 
    // an object including a `then` method
    // (another promise)
    // and the resolution of the inner promise
    // becomes the resolution of the outer promise
    return resolve(Promise.resolve(42));
  });

  console.log(`Resolution with: ${value}`);
})();

// log: Resolution with: 42
``` 

### A surprising edge-case

You have to be very careful when using the `then`-hack, there might be a case where the resolution process leads to unexpected behavior.

```javascript
Promise.resolve({
  then: resolve => resolve(42),
  foo: 'bar'
}).then(value => console.log(`Resolution with: ${value}`));

// log: Resolution with: 42
```

Even though the promise above resolves with an object including several properties all you get is `42`.

## The dynamic import is no exception and following the standard promise resolution process

When you use the dynamic `import` function to load JavaScript modules, `import` follows the same process because it returns a promise. The resolution value of the imported module will be an object including all the exported values and methods. 

For the case that you export a `then` function the specified promise handling kicks in to evaluate what the overall resolution should be. The `then` function can overwrite everything else that could be included in this module. 

```javascript
// file.mjs
export function then (resolve) {
  resolve('Not what you expect!');
}

export function getValue () {
  return 42;
}

// index.mjs
import('./file.mjs').then(
  resolvedModule => console.log(resolvedModule)
);

// log: Not what you expect
```

I'll definitely avoid naming my functions `then`. Finding a bug like this could take a few minutes. 🙈

And that's it for today! I hope that was useful and talk soon. 👋