Promises and Event Handlers

Promises are the basic unit of asynchronous programming in the Web. Promises are tasks, and they have two basic fundamental properties: they start when you create them and they execute only once. These two points are fine for doing this like doing a fetch() over ajax, but they drastically reduce the utility for certain types of task-based programming. So I created two new classes to handle these cases, but keeping the same API (except for the deletion of the troublesome onrejected method): the deferred and the repeatable promise.

The deferred promise is a wrapper around a regular javascript promise that allows it to be triggered manually at any point after it's creation. Create a deferred promise using const deferred = new DeferredPromise().then(t => something(t)).catch(c => otherthing(c)) and then, when you want to call it, call deferred.resolve('some data') (or reject()).

The repeatable promise is really only a promise factory having the promise API. When you resolve or reject the repeatable promise, it creates a new promise according to your configured tasks and resolves or rejects it. This makes it suitable as an event emitter, which ordinary promises are not, because of the resolve once behavior.

The onrejected() method is deleted because it only creates bugs. If you use onfulfilled() and onrejected(), if you were inexperienced you might think that errors in onfulfilled() would bounce to onrejected(). You would be wrong. Experienced JS programmers recommend not using onrejected(). Just use catch() like everyone else.

Deferred promises can be awaited by accessing the .output property, which exposes the internal promise. Repeatable promises cannot be awaited, because await implies a single run through of the code. However, if you build a single promise using build() (repeatables are just factories), you can await the result.

Promises are very similar to events, but not entirely useful because of the "run once right now" behavior. But once this is resolved, you get a very nice API for configuring repeated events, and this brings us to the EventHandler. You create an event emitter, subscribe one or more delegates (methods) to it using the promise API, and when the event emitter is invoked, those delegates are executed. This is called an EventHandler (even though it is an emitter) because when I was looking for an alternative to the nightmare caused by using custom browser events, I stole an idea from C#, and that's what they call it there.

Some async events will populate this area.

Link to the demo script
Class Method Example Detail
DeferredPromise ctor(onfulfilled?, throwOnUnhandledError = false) const promise = new DeferredPromise(result => result.map(m => someFunction(m)))

You create a deferred promise by calling new DeferredPromise(). Both inputs are optional. Just like with a normal promise, you can pass an onfulfilled function, which is the same as calling then() on the promise. Unlike a normal promise, you cannot pass an onrejected function, because this can lead to errors (exceptions in this method are not caught).

If you like, you can set throwOnUnhandledError to true. This automatically adds a catch() after every then() and catch() that throws a new error.

DeferredPromise then(onfulfilled?, throwOnUnhandledError = this.throwOnUnhandledError) promise.then(result => result.map(m => someFunction(m)))

Adds a callback to the promise, to be called upon successful completion of the previous callback. As with the constructor, both inputs are optional and act the same way, and as with the constructor, onrejected is omitted. Otherwise, this is the same as Promise (it's just a facade over the actual Promise class).

DeferredPromise catch(onrejected?, throwOnUnhandledError = this.throwOnUnhandledError) promise.catch(result => result.map(m => logTheError(m)))

Adds a callback to the promise, to be called upon failed completion of any previous callback. As with the constructor, both inputs are optional. catch() acts the same as it does on default promises, and throwOnUnhandledError acts in the same way as in then(). Otherwise, this is the same as Promise (it's just a facade over the actual Promise class).

DeferredPromise resolve(args?) promise.resolve(['this', 'is', 'something'])

Resolves the promise using the arguments provided. At this point, the internal promise is resolved, calling all the then() and catch() callbacks as configured. Note that the static Promise.resolve() method, which creates a new promise and resolves it right now, is not included on DeferredPromise, because that wouldn't be deferred.

DeferredPromise reject(args?) promise.reject('BAAAAKA!')

Rejects the promise using the arguments provided. At this point, the internal promise is rejected, calling the catch() callback as configured.

DeferredPromise output const result = await promise.output;

The output field returns the internal promise. In javascript, async/await is just syntactic sugar around promises, so if you want to resolve the deferred promise and await it, you need to access the actual promise, not a wrapper object. By awaiting myDeferredPromise.output, the browser is satisfied, invokes the promise, and waits for the output.

RepeatablePromise ctor(onfulfilled?, onUnhandledError?, throwOnUnhandledError = false) const promise = new RepeatablePromise(result => result.map(m => someFunction(m)))

You create a repeatable promise by calling new RepeatablePromise(). All inputs are optional. Just like with a normal promise, you can pass an onfulfilled function, which is the same as calling then() on the promise. Unlike a normal promise, you cannot pass an onrejected function, because this can lead to errors (exceptions in this method are not caught).

If you like, you can pass a default error handler, which adds a catch() to the very end of the callback chain.

Also if you like, you can set throwOnUnhandledError to true. This automatically adds a catch() at the very end (even after onUnhandledError) that throws a new error.

RepeatablePromise then(onfulfilled?, throwOnUnhandledError = this.throwOnUnhandledError) promise.then(result => result.map(m => someFunction(m)))

Adds a callback to the promise, to be called upon successful completion of the previous callback. As with the constructor, both inputs are optional and act the same way, and as with the constructor, onrejected is omitted. Otherwise, this is the same as Promise (it's just a factory that builds a DeferredPromise, which is just a facade over Promise).

RepeatablePromise catch(onrejected?, throwOnUnhandledError = this.throwOnUnhandledError) promise.catch(result => result.map(m => logTheError(m)))

Adds a callback to the promise, to be called upon failed completion of any previous callback. As with the constructor, both inputs are optional. catch() acts the same as it does on default promises, and throwOnUnhandledError acts in the same way as in then(). Otherwise, this is the same as Promise (it's just a factory that builds a DeferredPromise, which is jsut a facade over Promise).

RepeatablePromise resolve(args?) promise.resolve(['this', 'is', 'something'])

Builds and resolves the promise using the arguments provided. At this point, a new promise is resolved, calling all the then() and catch() callbacks as configured. Note that the static Promise.resolve() method, which creates a new promise and resolves it right now, is not included on RepeatablePromise, because that wouldn't be repeatable.

RepeatablePromise reject(args?) promise.reject('BAAAAKA!')

Builds and rejects the promise using the arguments provided. At this point, a new promise is rejected, calling the catch() callback as configured.

RepeatablePromise build() const deferred = promise.build();

Builds and returns a new deferred promise, using the current callback chain on the repeatable.

This can be awaited as in the following example: await promise.build().output;. This builds a single instance (you can't await multiple) and awaits it. I don't think it very likely that anyone will really need this, but it's just a side effect of the factory process.

I considered, for the sake of consistency, making an output field that returned the output of a built instance, but it seemed silly. I may change my mind, as it's a few minutes work.

EventHandler ctor(disableAsync = false) const emitter = new EventHandler();

Creates a new EventHandler. By default, callbacks are called using Promises (actually DeferredPromises) on the microtask queue, but if you set disableAsync to true, they'll be called synchronously. This makes it a lot easier to step through code in developer tools, but browsers really work better using asynchronous events.

EventHandler subscribe(delegate)
subscribe(callback, thisArg)
emitter.subscribe(observer.someMethod, observer)

Adds the callback to the event handler's delegates. If you subscribe the delegate of another event handler, then when the event handler is invoked, the delegate callbacks are also called, recursively.

EventHandler unsubscribeCallback(callback) emitter.unsubscribeCallback(observer.someMethod)

Searches the event handler's delegates and removes instances of the callback.

EventHandler unsubscribeListener(observer) emitter.unsubscribeListener(observer)

Searches the event handler's delegates and removes instances of the observer's delegate or callbacks where thisArg is set to the observer.

EventHandler unsubscribeDelegate(observer) emitter.unsubscribeDelegate(observer)

Searches the event handler's delegates and removes instances of the observer's delegate.

EventHandler delegate const delegates = emitter.delegate

Exposes the delegates (which can be recursive references to other event handlers' delegates) of the event handler. It's main use is being subscribed to other event handlers, as in this example, where events on emitter2 trigger all callbacks of emitter1:

const emitter1 = new EventHandler();
const emitter2 = new EventHandler();
emitter1.subscribe(myClass.onClick, myClass);
emitter2.subscribe(emitter1.delegate);

EventHandler invoke(args) emitter.invoke(args)

Calls (either synchronously or asynchronously, using the RepeatablePromise class, depending on the disableAsync setting) all the delegate callbacks that are subscribed, supplying the arguments supplied to the invoke() method.

EventHandler clear() emitter.clear()

Remove all delegates from the emitter's delegate list.

EventHandler find({ callback?, thisArg?, firstMatch? }) emitter.find({callback: myClass.onClick, thisArg: myClass})

Returns an array containing any delegates matching either callback, thisArg, or both (if provided). If firstMatch is true, the search stops after the first match. Does not consider non-callback delegates. Does not recursively search other event handlers' delegates.

Type definitions