DEV Community

Cover image for πŸ” createSharedPromise: Share In-Flight Async Calls with Elegance
Ivan Karbashevskyi
Ivan Karbashevskyi

Posted on

πŸ” createSharedPromise: Share In-Flight Async Calls with Elegance

Have you ever triggered the same async request multiple times in parallel β€” like fetching a config or user profile β€” and wondered:

β€œWhy are we hitting the server or database more than once when the data is the same?”

This pattern is common across servers and frontends, and it’s inefficient.

Let’s solve that with a super lightweight, zero-dependency, elegant utility:
createSharedPromise() β€” now optimized with minimal code, closure-based logic, and optional one-time delivery.


πŸš€ What Does It Do?

β€’ Shares one Promise across multiple .then() calls
β€’ Deduplicates parallel requests
β€’ Caches the result after the first call
β€’ Allows clearing the cache
β€’ Fully thenable (works with await too)
β€’ Optional { once: true } to flush callbacks after resolution


🧠 The Code (fully inline, zero-dependencies)

function createSharedPromise(asyncFunction, { once = false } = {}) {
  let inFlight = false;
  let cache = null;
  const fulfillRejectHandlers = [];

  const run = (argument, fulfillRejectFlag) => {
    if (once) {
      while (fulfillRejectHandlers.length) {
        const { [fulfillRejectFlag]: handler } = fulfillRejectHandlers.pop();
        handler?.(argument);
      }
    } else {
      fulfillRejectHandlers.forEach(
        ({ [fulfillRejectFlag]: handler }) => handler?.(argument)
      );
    }
  };

  return {
    clear() {
      cache = null;
    },
    then(onFulfilled, onRejected) {
      if (cache !== null) {
        onFulfilled?.(cache);
        return;
      }

      fulfillRejectHandlers.push([onFulfilled, onRejected]);

      if (inFlight) return;
      inFlight = true;

      asyncFunction()
        .then(result => {
          cache = result;
          inFlight = false;
          run(result, 0);
        })
        .catch(error => run(error, 1));
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

This version is tiny, elegant, and avoids unnecessary Promise wrapping or internal get() method exposure.


πŸ§ͺ Try It Live in DevTools

function mockSlowFetch() {
  console.log('[mockFetch] Starting...');
  return new Promise(resolve =>
    setTimeout(() => {
      const res = { time: new Date().toISOString() };
      console.log('[mockFetch] Done:', res);
      resolve(res);
    }, 2000)
  );
}

const sharedFetcher = createSharedPromise(mockSlowFetch);

console.log('Calling sharedFetcher 3 times:');
sharedFetcher.then(r => console.log('Result 1:', r));
sharedFetcher.then(r => console.log('Result 2:', r));
(async () => {
  const r = await sharedFetcher;
  console.log('Result 3:', r);
})();

setTimeout(() => {
  console.log('Calling again (cached):');
  sharedFetcher.then(r => console.log('Result 4 (cached):', r));
}, 3000);

setTimeout(() => {
  console.log('Clearing and calling again...');
  sharedFetcher.clear();
  sharedFetcher.then(r => console.log('Result 5 (new):', r));
}, 6000);
Enter fullscreen mode Exit fullscreen mode

🧩 Real-World Use Cases

1. πŸ“¦ Vanilla Node.js Server

import http from 'http';
import fs from 'fs/promises';

const configLoader = createSharedPromise(() =>
  fs.readFile('./config.json', 'utf-8').then(JSON.parse)
);

http.createServer(async (req, res) => {
  if (req.url === '/config') {
    configLoader.then(config => {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(config));
    });
  }
}).listen(3000);
Enter fullscreen mode Exit fullscreen mode

2. 🧱 NestJS Service (shared DB/API calls)

@Injectable()
export class ConfigService {
  private configFetcher = createSharedPromise(() =>
    this.httpService.get('https://api.example.com/config').toPromise().then(r => r.data)
  );

  getConfig(): Promise<any> {
    return this.configFetcher;
  }

  refresh() {
    this.configFetcher.clear();
  }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

await this.configService.getConfig();
Enter fullscreen mode Exit fullscreen mode

3. 🧩 Angular Service

@Injectable({ providedIn: 'root' })
export class UserService {
  private profileFetcher = createSharedPromise(() =>
    this.http.get('/api/profile').toPromise()
  );

  constructor(private http: HttpClient) {}

  getProfile(): Promise<any> {
    return this.profileFetcher;
  }

  refreshProfile(): void {
    this.profileFetcher.clear();
  }
}
Enter fullscreen mode Exit fullscreen mode

Use in component:

this.userService.getProfile().then(profile => this.profile = profile);
Enter fullscreen mode Exit fullscreen mode

4. 🌍 Vanilla JS Frontend

<button id="loadConfig">Load Config</button>
<script type="module">
  const configFetcher = createSharedPromise(() =>
    fetch('/config.json').then(res => res.json())
  );

  document.getElementById('loadConfig').addEventListener(() => {
    configFetcher.then(config => {
      console.log('Loaded config:', config);
    });
  });
</script>
Enter fullscreen mode Exit fullscreen mode

πŸ”„ Optional: { once: true }

If { once: true } is passed, the callback queue will be flushed once, and not retained β€” useful for cases where memory usage matters, or when only one delivery is expected.

const oneTime = createSharedPromise(() => fetch('/data'), { once: true });
Enter fullscreen mode Exit fullscreen mode

βœ… Benefits Recap

βœ… Promise deduplication
βœ… .then() compatible
βœ… await support (via then)
βœ… Manual cache clear (.clear())
βœ… One-time callbacks ({ once: true })
βœ… Zero dependencies


🧠 Final Thoughts

This is an elegant solution for a common problem that can lead to:
β€’ Better performance
β€’ Lower server/API usage
β€’ Cleaner architecture

Use it to wrap:
β€’ HTTP calls
β€’ DB queries
β€’ Config loaders
β€’ Feature flags
β€’ Anything expensive and cacheable!

Top comments (0)