JavaScript
API Development
Synchronous Programming
Asynchronous Programming
Library Design

What is a good approach to develop a synchronous/blocking and an asynchrounous/non-blocking library-api in parallel? JavaScript

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

If a JavaScript library needs both synchronous and asynchronous APIs, the main goal is to avoid implementing the same business logic twice. The cleanest design is usually a shared pure core plus thin sync and async wrappers only where the underlying runtime actually supports both execution models.

Keep the Core Logic Shared

The easiest way to create maintenance problems is to build one full sync implementation and one full async implementation that gradually drift apart. Instead, isolate parsing, validation, transformation, and domain rules in shared pure functions.

javascript
1function parseConfig(text) {
2  const data = JSON.parse(text);
3
4  if (!data.name) {
5    throw new Error("Missing name");
6  }
7
8  return data;
9}

Now the only difference between sync and async entry points is how the text arrives:

javascript
1const fs = require("fs");
2const fsPromises = require("fs/promises");
3
4function loadConfigSync(path) {
5  const text = fs.readFileSync(path, "utf8");
6  return parseConfig(text);
7}
8
9async function loadConfig(path) {
10  const text = await fsPromises.readFile(path, "utf8");
11  return parseConfig(text);
12}

That keeps the API surface broader without duplicating the actual library rules.

Do Not Offer Sync APIs for Inherently Async Work

This is the most important design constraint. A synchronous API makes sense only if the platform can really do the work synchronously.

Reasonable sync cases:

  • CPU-bound parsing
  • in-memory transformations
  • Node.js filesystem utilities for scripts or tooling

Bad sync cases:

  • browser networking
  • server-side operations that would block the event loop badly
  • any runtime where the underlying resource is only available asynchronously

In other words, do not invent fake blocking wrappers around promise-based work just for API symmetry. In JavaScript, that usually creates worse ergonomics, not better.

Let Async Be the Primary I/O API

For modern JavaScript libraries, the async API should usually be the main public interface when I/O is involved. Promises compose naturally with the rest of the ecosystem and are the least surprising choice for application code.

javascript
1async function fetchAndParse(fetchImpl, url) {
2  const response = await fetchImpl(url);
3
4  if (!response.ok) {
5    throw new Error(`Request failed: ${response.status}`);
6  }
7
8  const text = await response.text();
9  return parseConfig(text);
10}

If a sync version also exists, it should be limited to cases where a synchronous source really exists, such as local file reads in Node.js.

Keep Naming Explicit

If you publish both styles, name them clearly. Common patterns are:

  • 'read() and readSync()'
  • 'load() and loadSync()'
  • 'parse() for pure CPU work and load() for I/O'

This is better than giving the sync and async variants nearly identical names and forcing users to infer behavior from documentation or return types. Clear naming also mirrors the conventions of Node.js core modules, which many JavaScript developers already expect.

Consider Inverting the Design for Better Separation

Another strong approach is to make the library core completely independent of I/O. Then the library exports only pure operations, and the application or adapter layer decides whether data is supplied synchronously or asynchronously.

That can look like this:

javascript
1function transformData(data) {
2  return data.map((value) => value * 2);
3}
4
5module.exports = { transformData };

Then a Node.js caller can use fs.readFileSync, while another caller can use fs.promises.readFile, and both pass data into the same core function. This design often produces the cleanest long-term architecture because the library never owns blocking versus non-blocking I/O in the first place.

Common Pitfalls

  • Writing full sync and async implementations that duplicate the same business logic.
  • Exposing a synchronous API for work that the runtime cannot really perform synchronously.
  • Making sync and async variants behave slightly differently on validation or error handling.
  • Designing API symmetry first and core architecture second.
  • Hiding blocking behavior behind a harmless-looking function name.

Summary

  • Share the core logic and keep sync versus async differences at the boundary.
  • Expose both APIs only when the underlying work genuinely supports both models.
  • Prefer async as the primary public API for I/O-heavy JavaScript libraries.
  • Use clear names such as readSync() so blocking behavior is obvious.
  • When possible, keep the library core pure and let callers decide how data is obtained.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design