Back
Apr 15, 2024

Lazy Promises in Node.js

TL;DR: use p-lazy for lazy promises.

Promise is a powerful tool in asynchronous programming that allows developers to call a time-consuming function and proceed with program execution without waiting for the function result. This can be useful if we want to perform something like a file system I/O, DB query, or an external API call without waiting for the result. Promise results can also be awaited - instantly and later during the program execution.

But what if we want to declare a promise, but not actually run it unless explicitly awaited? These cases are situational, but not at all uncommon. Today we will learn how to implement such an approach and discuss why it can be useful and even powerful.

Why?

Let us consider a database connection.

const connection = new Promise(someHeavyConnectionStuff);
// ......
(await connection).query("SELECT ...");

Ideally, we don’t want to connect to the database until it's necessary. However, in the snippet above we will, because promise will start executing as soon as it is declared. This way, we will initiate the DB connection long before the actual call is made.

While it makes perfect sense in terms of Promises, that’s not something we are going for, so it would be great to be able to use lazy promises when we need them.

How?

In the ECMAScript, there is a concept of a “thenable“. It is an interface with a .then() method, which accepts a callback as an argument. Because of a diverse Node.js ecosystem, there is more than one Promise or promise-like class, so ECMAScript treats them all as equal. Despite their internal workings, as soon as they are complete (e.g. api call or db query return results) - .then() callback is called. await keyword is just a syntax sugar that covers this .then() call. So, these two ways to handle promise are virtually the same:

async const processAsyncCall = (filters) => {

  myAsyncCall().then(result => console.log(result));
  
  const result = await myAsyncCall();
  
}

 

This drives us to a conclusion: we can implement a “thenable“ by ourselves, and it will work both with .then() and await as long as we implement .then() function.

How is this different from a regular Node.js Promise? Let us see this usage example. P-lazy has been utilized for lazy promises in this example since this is a production-ready library:

const timeConsumingCode = resolve => {
    // some time-consuming code
  }

async const processAsyncCall = (filters) => {
  // myRegularPromise runs code as soon as new Promise constructor is called
  const regularPromise = new Promise(timeConsumingCode);
  
  // here we ensure the result is completed. We can also use .then()
  const regularPromiseResult = await regularPromise;
  
  // timeConsumingCode has not been called so far
  const lazyPromise = new PLazy(timeConsumingCode);
  
  // timeConsumingCode is run when awaited
  const lazyPromiseResult = await lazyPromise;
  
}

This way, we can call async code only if it is awaited.

As already mentioned, you can use p-lazy to get up and running with lazy promises. But if you want to know how it works, let us have a look at the implementation:

export default class PLazy extends Promise {
    #executor;
    #promise;

    constructor(executor) {
        super(resolve => {
            resolve();
        });

        this.#executor = executor;
    }

    // ...
    // some methods to support all Promise interfaces
    // ...

    then(onFulfilled, onRejected) {
        // TODO: Use `??=` when targeting Node.js 16.
        this.#promise = this.#promise || new Promise(this.#executor);
        return this.#promise.then(onFulfilled, onRejected);
    }
}

As you can see, the constructor does not really do anything. It only stores the executor, which is a function to be called when awaited. At this point, a regular promise would already start executing it, but we don’t need to unless the promise is awaited. And to actually resolve the lazy promise, .then() method is described. Under the hood, it creates a promise and resolves it.

So, long story short, a lazy promise in Node.js is a class that perfectly mimics the Promise interface (or at least Thenable), and creates the actual promise only when awaited.

And, coming back to our initial example, the lazy Promise for DB connection would look like this:

const connection = new PLazy.from(someHeavyConnectionStuff);

// ......
(await connection).query("SELECT ...");

I hope this article helped you to learn something new about promises and how to cook them. Happy coding!

Subscribe for the news and updates

More thoughts
Mar 26, 2025Technology
Common Mistakes and Recommendations for Test Cases

The article highlights common test case mistakes, offers ways to fix them, and provides practical tips to improve and optimize test cases.

Feb 3, 2025Technology
Figma for Developers: What Dev Mode Offers and How to Use It

This article explores Figma’s Dev Mode, a tool that streamlines design-to-code translation by enabling precise inspection, automated code generation, and seamless integration with design systems.

Dec 22, 2024Technology
Python and the Point Rush in DeFi

This article demonstrates how to use Python to automate yield calculations in decentralized finance (DeFi), focusing on the Renzo and Pendle platforms. It guides readers through estimating potential rewards based on factors like token prices, liquidity, and reward distribution rules, emphasizing the importance of regular data updates and informed decision-making in DeFi investments.

Nov 27, 2024Technology
Stoicism At Work

This article explores how Stoic principles can be applied in the workplace to navigate stress, improve self-control, and focus on what truly matters, with practical examples from the author’s experience in software development.

Aug 27, 2024Technology
An Effective Preparation Algorithm for ISTQB Certification

This article offers key insights into the ISTQB certification and shares a proven preparation strategy to help candidates succeed.

Apr 11, 2024Technology
Test Analysis at the Feature Level

In the previous article, we learned about test analysis for a product in general, and now we are ready to go further and look at test analysis for specific features.