Promise-Based Modelling of Asynchronous Flows and When Deferred Makes Sense
Promises are a staple in JavaScript development, and most of the time we engage with them as consumers. Whether it's a fetch request from the browser or a utility from a library, we typically attach callbacks and wait. As consumers, we have no authority over when the promise settles — that responsibility lies with the producer. We merely observe and react. There is no direct mechanism to alter the state of a promise from the outside; we can only chain off of it. That said, it is possible to alter the outcome of a chained promise: inside a promise.then() callback, throwing an error will effectively reject the downstream promise created by .then. The runtime constructs this promise implicitly, and we decide its fate without ever creating it explicitly.
Producing promises, on the other hand, is a rarer activity. When we do create one, we are given an executor function with resolve and reject parameters. But what happens when the resolution criteria are unknown at creation time? What if the promise should only settle based on an event that occurs much later? The executor gives us these functions, but only within its scope. This is the exact scenario we will explore.
Explicit promise creation is uncommon because most asynchronous operations are already wrapped — the browser's fetch API, for instance, returns a promise for us. Still, there are legitimate cases where we might need to create promises ourselves:
- Converting a callback-based function into a promise-based one.
- Wrapping long-running tasks with
setTimeoutorsetIntervaland exposing them as promises. - Augmenting an existing promise — for example, adding retry logic or altering the original resolution condition. Often this can be achieved via chaining, but not always.
- Normalising a function that can return a plain value, say
fn1, to match the signature of another that returns a promise,fn2, so consumers don't need to differentiate. - Modelling flow control in user interfaces. For instance:
– waiting for user interaction before proceeding in a sequence
– allowing an action that may return immediately or may need to wait, depending on the scenario, and wrapping it in a promise so the consumer always deals with the same interface
In this piece, we focus on the last point — modelling asynchronous UI flows with promises.
All code samples are in React, but the principles apply to JavaScript generally.
Restating the core challenge:
When we create a promise, we have full control over its settlement inside the executor. But what if we need to settle it from elsewhere? Say we create a promise now and want to resolve it upon a future button click. The obvious approach is to place the event handler inside the executor, where resolve is in scope. Alternatively, we can assign resolve to an outer variable and call it later. The deferred pattern formalises this second approach, bundling the promise and its resolve/reject functions into one object.
STEP 1:
Start simple. Imagine an e-commerce product page where users pick a quantity and place an order. A button triggers the order via an onClick handler, which calls a handleClick function and then placeOrder.
STEP 2:
Now add a confirmation dialog. We move the placeOrder call from the Order button to the Yes button in the confirmation UI. The Order button simply shows the dialog; confirmation triggers the actual order.
STEP 3:
This works, but confirmation every time is unrealistic. We only want to prompt when the quantity exceeds a threshold. Based on the entered quantity, the Order button handler either calls placeOrder directly or first shows the dialog and then orders on confirmation.
STEP 4:
The logic is still manageable, but we now call placeOrder in multiple places. For clarity and separation of concerns, it would be better if the Order button owns the entire flow, keeping the call inside its onClick only.
Consider a more elaborate flow: if the quantity is over 400, show a dialog, then fetch the maximum available stock from the server, and only then order. That's a lot of nesting. More generically, think of the Order button as a reusable component that doesn't expose placeOrder. Each page using it returns a boolean to indicate whether to proceed. The component only cares about that decision. In such cases, the usual redirect pattern breaks down. This is where promises come in.
What if the Order button could pause execution until any prerequisite condition — user confirmation, API response, whatever — is satisfied, and then proceed with placeOrder?
What if we create a promise inside the Order button and let the responsible party settle it?
That idea is worth exploring.
STEP 5:
This approach is functionally sound, but the implementation is clunky. Since resolve and reject are only available inside the executor, we end up defining handleConfirmation and handleCancel within it. Let's improve that.
STEP 6:
By storing references to resolve and reject outside the executor, we can call them from anywhere. This lets us move our handler functions out, making the code cleaner.
But can we abstract this pattern further? What if we had a reusable object that creates a promise and automatically keeps resolve and reject accessible? That's exactly what a deferred is — an object that encapsulates a promise and governs its lifecycle.
We define a Deferred constructor and instantiate it inside the Order button handler. This abstracts away both promise creation and the manual bookkeeping of resolve and reject.
Of course, other patterns exist, particularly within the framework ecosystem, to handle this kind of flow. One could spread the ordering logic across components, but the deferred approach keeps the responsibility contained within the Order button, making the flow more readable and maintainable.
This scenario highlights the value of deferred objects. Whenever we produce promises without knowing, at creation time, how they'll settle, the deferred pattern gives us a clean way to hold onto resolve and reject and settle the promise later.
NOTE: The implementation shown here is a minimal proof-of-concept. For production use, more robust implementations are available.
References/Further reading:
https://stackoverflow.com/questions/17308172/deferred-versus-promise
https://medium.com/front-end-weekly/advanced-react-anti-patterns-a644c55437fe
