Exposed Promises?

Yes, you are not supposed to use promises for that. A Promise represents the future value of the request - but the request itself can't be represented solely by a promise, because "the request" encapsulates the work itself, which a Promise does not.

In other words, you want an entirely distinct primitive - let's call it a Task - that produces a Promise but is not, itself, a Promise.

Maybe, yes!

Maybe reword: "multiple primitives (parallel "Tasks") that produce 1 Promise, but are not, themselves, Promises."

Imagine functions everybodyUseThisData(), everybodyDealWithThisError(), everybodyLoseInterest():

  • called from independent .js modules
  • attached to independent async code paths

And: ifYouGetSomeData( thenDoThis: function )
Guaranteed thenDoThis() will be called exactly 1 or 0 times.

Maybe, but... am I really not supposed to use Promises?
If I build my own flow control using the above functions, it behaves identically to exposed Promises, but loses async / await.

When it comes to cancelling a promise, I may need something what GitHub - lwr/EPromises: A simple promise extension with tuple state and centralized error handling supports has provided, in the case when we have handled some error in a callback method and hope to silently reject the promise:

EPromise.resolve(1).then(v => v !== 1
    || EPromise.reject('should not throw 1')); // => throw "Uncaught (in promise): should not throw 1"

EPromise.resolve(1).then(v => v !== 1
    || console.log('should not throw 1') + EPromise.abort()); // => "should not throw 1"

In the snippet above, EPromise.abort means to convert the promise state from fulfilled to rejected, and do not trigger the event: Window: unhandledrejection event - Web APIs | MDN

Reversely, we can also handle some error in a callback method and rethrow some when they are not unhandled:

EPromise.reject(-1).then(() => {}, v => v !== -1
    ? console.log('handle when not rejected with -1')
    : EPromise.rethrow()); // => throw "Uncaught (in promise): -1"

Thanks for the code snippet @0X-JonMichaelGalindo - I'm going to try and parse it to the best of my abilities and offer an alternative solution. Obviously, I don't know a whole lot about your code besides what you gave, so it could be that what you did really is the best solution, and what I'm presenting is just garbage. But, it's another way to think about the problem.

// util.js

// I often use a little event-emitter helper like this in my code
export function createEventEmitter() {
  const listeners = []
  return {
    subscribe(fn) {
      listeners.push(fn)
      let unsubscribed = false
      return function unsubscribe() {
        if (unsubscribed) return
        const index = listeners.indexOf(fn)
        listeners.splice(index, 1)
        unsubscribed = true
      }
    },
    trigger(...args) {
      return listeners.map(fn => fn(...args))
    }
  }
}


// yourStuff.js

import { createEventEmitter } from './util.js'

const onSolutionFound = createEventEmitter()
const onLogout = createEventEmitter()
const onConnectionClose = createEventEmitter()

websocket.send( 'some request' );

api.somePromise = new Promise((resolve_, reject_) => {
  let unsubscribeHandlers = []
  const unsubscribeAll = () => unsubscribeHandlers.forEach(fn => fn())
  const resolve = (...args) => { unsubscribeAll(); resolve_(...args) }
  const reject = (...args) => { unsubscribeAll(); reject_(...args) }

  const onMessage = () => {
    if( isSomething( message ) ) resolve( /* If anyone cares, here's the answer. */ )
    if( isOther( message ) ) reject( /* If anyone cares, abort this. */ )
  }
  websocket.addEventListener('message', onMessage)

  const unsubscribeHandlers = [
    () => websocket.removeEventListener('message', onMessage)
    onSolutionFound.subscribe(({ success }) => {
      if (success) {
        resolve( /* My answer might be ignored. Use if desired. */ )
      } else {
        reject( /* If this is still alive, it needs to abort. */ )
      }
    }),
    onLogout.subscribe(() => {
      reject( /*...*/ )
    }),
    onConnectionClose.subscribe(() => {
      reject( /* ... */ )
    })
  ]
})

//...
websocket.onmessage = message => {
  //...
}
//...
websocket.onclose = () => {
    onConnectionClose.trigger()
}
//...
api.logout = () => {
  //...
  onLogout.trigger()
}

//...
// code that must only execute if nothing cancels somePromise
api.somePromise.then( /*...*/ );

// independent code paths racing to provide a solution or reason for cancellation:
if( /*...*/ ) {
  onSolutionFound.trigger({ success: true })
}

//...
if( /*...*/ ) {
  onSolutionFound.trigger({ success: false })
}

You'll notice that I'm finding different ways to move the promise-related logic together, instead of having it spread out throughout the entire module. This (perhaps) makes it easier to figure out what will cause that promise to resolve and reject (you just have to look at once place), and (perhaps) makes the code easier to reason about.

1 Like

On a different note, here's an article that got shared on these forms at a different time (I can't remember who shared it), that I found very interesting. It's about different forms of promise cancelation, and why cancel tokens really are the most robust form. (Many other versions have different issues in different scenarios)

1 Like

Thanks. That is a solid technique, and very useful.
"figure out what will cause that promise to resolve and reject" is not the development approach I've personally decided to rely on.

At this point, I am convinced that Promises should not be exposed by default, but I am also convinced that there is an opportunity for JS to offer some API that addresses cases like mine.

I will let this idea go (still using it personally for now). Maybe in the future, with more experience and development, I can offer something more helpful.

I have to deal with interdependent, parallel, distributed, redundant processes. I have to write code that:

  1. Knows what kind of results it should try to achieve
  2. Needs to initiate and await network events to achieve its results
  3. Doesn't know when or if it will be executed
  4. Doesn't know what kind of data to expect (hints)
  5. Doesn't know under what conditions it needs to halt
  6. Doesn't know how/whether its results will/should be used

My approach is about fail-safety, recovery, and race-based, latest-based, or priority-based guarantees.
Some lessons learned:

  1. Interdependent code modules will never start an unknown-length loop
  2. Interdependent code modules will never time-schedule future code execution
  3. Interdependent code modules will never accumulate state data over time

Thanks for taking the time to help me understand Promises better!

1 Like