dy_
dy_

Reputation: 7003

Can I await a thenable object that resolves to itself without trapping execution in an infinite loop?

Trying to come up with API, mixing in promise functionality like:

class Awaitable {
  constructor () {
    this.promise = Promise.resolve()
  }
  then (fn) {
    // awaited result must be _this_ instance
    return this.promise.then(() => fn(this))
  }
}

let smth = await (new Awaitable())
console.log(smth)

This code creates recursion. The main point is to have smth to be the newly created thenable instance.

Stubbing then with null makes awaited result incomplete.

I wonder if that's possible at all, seems like there's some conceptual hurdle, I can't wrap my head around.

Upvotes: 1

Views: 423

Answers (2)

dy_
dy_

Reputation: 7003

The correct solution

Symbol.thenable proposal.

import { parse as parseStack } from 'stacktrace-parser' 

class Awaitable {
  constructor () {
    this.promise = Promise.resolve()
  },

  [Symbol.thenable]: false,

  then(fn) {
    this.promise.then(() => fn(this))
    return this
  }
}

let smth = await (new Awaitable())
console.log(smth.then) // function

Fragile non-standard solution

Detecting if thenable instance is called by await is possible by parsing callstack. Here's solution based on stacktrace-parser package:

import { parse as parseStack } from 'stacktrace-parser' 

class Awaitable {
  constructor () {
    this.promise = Promise.resolve()
  }
}

Object.defineProperty(Awaitable.prototype, 'then', {
  get() {
    let stack = parseStack((new Error).stack)
    
    // naive criteria: if stacktrace is leq 3, that's async recursion, bail out
    // works in webkit, FF/nodejs needs better heuristic
    if (stack.length <= 3) return null
    
    return (fn) => {
      this.promise.then(() => {
        fn(this)
      })
      return this
    }
  }
})

let smth = await (new Awaitable())
console.log(smth.then) // function

The heuristic must be enhanced for FF/nodejs, to solidify - that'd require sort of static analysis wizardy.

Upvotes: -1

Bergi
Bergi

Reputation: 665276

It makes no sense for a promise (or any thenable) to fulfill with itself. If you already had the object, you wouldn't need to wait for it.

Resolving a promise does automatically unwrap thenables, which would create infinite recursion in your case where it would resolve with itself, and you can (unfortunately) not avoid that. So just don't attempt to do it. Fulfill your promise with nothing (undefined) instead.

class Awaitable {
  constructor () {
    this.promise = Promise.resolve(undefined); // simplified
  }
  then(onfulfill, onreject) {
    return this.promise.then(onfulfill, onreject);
  }
  // other methods
}

const smth = new Awaitable();
await smth; // works just fine now
console.log(smth);

Upvotes: 0

Related Questions