Photo of Joakim Hedlund

Joakim Hedlund

Web enthusiast

Execute a promise only when awaited

Last updated

This is probably only useful if you're writing query builder, but I asked myself if it was possible to write a class that would construct an object and not have to expose something like a run() method. Turns out it is!

So, the goal is to let us write something like:

const query = new UserQuery()

query
    table('users')
    .where('name', 'John')
    .where('age', 31)

const results = await query

Most libraries would have us write await query.execute() or similar, but the beauty of thenables let's us circumvent this. How?

Our base class

/**
 * A base class that only executes once, once you `await` it.
 * You could call it a Lazy Singleton Promise.
 */
class ThenableExecutor {

  // Implement your own execute() method like this one.
  /* abstract */ execute() {
    return new Promise((resolve) => {
      console.log('Promise is running');
      setTimeout(() => { resolve('ResolvedValue') }, 1500)
    })
  }

  _hasExecuted = false
  _promise

  then(resolve) {
    if (!this._hasExecuted) {
      this._hasExecuted = true
      this._promise = this.execute()
    }
    resolve(this._promise)
  }
}

module.exports = exports = ThenableExecutor

This base class does two things;

  • it calls execute() when someone interacts with it as a promise, i.e. by using await or then
  • It makes sure you only call the execute function once, no matter how many times you await it

The query builder

Now let's use our ThenableExecutor class to write a class that will construct the query up until the point when you run it.

const ThenableExecutor = require('./ThenableExecutor')

/**
 * A sample query builder that only executes the query once you await it
 */
class PaymentUpdateActionRequest extends ThenableExecutor {

  constructor(paymentId, version) {
    super()
    this.endpoint = `/payments/${paymentId}`
    this.version = version
  }
  actions = []

  execute() {
    console.log(
      'Making 1 HTTP request',
      JSON.stringify({
        url: this.endpoint,
        method: 'POST',
        headers: {
          Authorization: `Bearer 1337`,
          'Cache-Control': 'no-cache',
          'Content-Type': 'application/json'
        },
        body: {
          version: this.version,
          actions: this.actions
        }
      })
    )
    return { some: 'data', from: 'backend' }
  }

  table(tableName) {
    this.actions.push({
      action: 'setKey',
      key
    })
    return this
  }

  where(field, value) {
    this.actions.push({
      action: 'setInterfaceId',
      interfaceId
    })
    return this
  }

}

module.exports = exports = PaymentUpdateActionRequest