quansync: one implementation, a sync and an async face
"Superposition" between sync and async
At a glance
- What is it?
- quansync is a small TypeScript library for writing a function once and exposing it with both a synchronous and an asynchronous interface, built on generator functions and yield*. It ships as ESM with no dependencies, and a companion unplugin macro offers the same thing with async/await syntax.
- Who is it for?
- quansync fits a library author who needs the same logic exposed synchronously and asynchronously without duplicating it, especially in Node where sync file and child process calls still have a place. It does not fit a codebase that never wants a blocking call, or a browser bundle where shipping both paths is pointless.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 36 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem: two APIs, one body of logic
quansync exists to remove a specific duplication. Plenty of Node libraries expose a synchronous function and an asynchronous twin, and the usual outcome is that the two implementations drift apart because somebody forgot to update the other. quansync lets you write the logic once and derive both faces from it. The README frames the idea as a superposition between sync and async, with the name coming from quantum plus sync, and describes the package as a way to create sync and async APIs with usable logic. The design constraints it advertises are typesafe, ESM and modern JavaScript, and zero dependencies. The project is heavily inspired by genasync by Logan Smyth, and the reasoning behind the whole approach is deferred to a blog post by Anthony Fu titled Async, Sync, in Between. The audience is library authors rather than application developers, which is why the README reads more like a design note than a tutorial.
Two implementations, or one generator, produce the same pair
There are two ways to build a quansync function. The first gives the library a sync and an async implementation of the same operation, and it wraps them into a single callable object with .sync and .async members. The second passes a generator function, and the library derives both behaviours from the generator body itself. In the generator form, calling another quansync function is done with yield*, and the result comes back as a plain value. The README wires a file reader in the first style, and this is the shape of it:
import fs from 'node:fs'
import { quansync } from 'quansync'
// Create a quansync function by providing `sync` and `async` implementations
const readFile = quansync({
sync: (path: string) => fs.readFileSync(path),
async: (path: string) => fs.promises.readFile(path),
})So the example wires a file reader in the first style, with a synchronous readFileSync call for the sync path and the promises API for the async path, then builds a second function on top of it in the generator style, calling the reader with yield* and prefixing the result. The consumer then chooses the face at the call site, using myFunction.sync for a blocking result or awaiting myFunction.async. The logic in between is written once.
getIsAsync tells the body which face is in use
A body written once often needs to know which mode it is running in, and quansync provides getIsAsync for exactly that. It returns a boolean indicating whether the current execution is in async mode, and the README example uses it inside a generator with yield* to log the value. The three call forms give three answers: calling the function synchronously returns false, awaiting the function directly returns true, and awaiting the explicit .async member returns true. That detail matters for code that has to branch, for instance to avoid awaiting something when the caller already intends to block. It is a small API, and it is the sort of thing that only makes sense once the rest of the model is understood, which is also why it is documented after the basic usage example rather than before.
all() runs several quansync calls and returns their results
The all helper is the concurrency primitive in the library, and it is again written as a generator, so it works identically in both modes. The README example defines a loadFiles function that returns yield* all of an array of readFile calls over three paths, then calls it synchronously and asynchronously to obtain two result sets. Because all is expressed with yield*, the same source produces a synchronous implementation that runs the operations one after another and an asynchronous one that can overlap them. The naming deliberately mirrors the built-in Promise.all, which makes the intent obvious to anyone who has used it, and the function is imported from the package root alongside quansync itself. This is the piece that turns quansync from a single-function convenience into a small concurrency toolkit, and it is the one to reach for when the same code needs to be blocking in one place and concurrent in another.
The unplugin macro trades generators for await
The README is direct about the cost of the generator syntax. If you would rather not use function* and yield*, the project provides a build-time macro through a separate package, unplugin-quansync, that lets you write quansync with async and await syntax while still being able to extract the sync version. The import changes to quansync/macro, the function you pass is an ordinary async function, and inside it you await other quansync calls rather than yielding them:
import fs from 'node:fs'
import { quansync } from 'quansync/macro'
// Create a quansync function by providing `sync` and `async` implementations
const readFile = quansync({
sync: (path: string) => fs.readFileSync(path),
async: (path: string) => fs.promises.readFile(path),
})The rest of that example The rest of the API is unchanged, so the same file still exposes a .sync member and an .async member. The catch is that this is a build-time transform, which means it only works if your bundler or build tool can run the unplugin, and the details live in the unplugin-quansync documentation rather than in this repository. The package exports map confirms the split, with a separate ./macro entry point alongside the main one.
Zero dependencies, ESM only, and a benchmark you have to run yourself
The package metadata is unusually explicit. It is type module, sideEffects is false, and the exports map points at ./dist/index.js, ./dist/macro.js and the package.json, with the published files limited to dist. The declared dependencies list is empty, which matches the README claim of zero dependencies, and the tooling around it is modern: pnpm 10.24.0 as the package manager, tsdown for the build, vitest for tests, eslint for linting, bumpp for releases and simple-git-hooks for the prepare script. There is a benchmark, and the README tells you how to run it with pnpm run build and pnpm run benchmark, which maps to node scripts/benchmark.js in the scripts. The stated result is that each yield carries roughly 150 ns of overhead, comparable to an awaited synchronous call, measured on an Apple M1 Max. That is a figure for the yield mechanism itself, not for any workload you would run on it, and the repository also ships a benchmark.md file for the detail.
Version 1.0 under MIT, with a short release history
quansync is MIT licensed, with the copyright shared by Anthony Fu and Kevin Deng, both of whom appear as author and contributor and as funding links on the package metadata. The version history is short. v0.2.11 was released on 2025-08-16, v0.3.0 on 2025-12-02 and v1.0.0 on 2025-12-04, so the project moved to 1.0 within about two weeks of the 0.3.0 tag. The last push to the repository was on 2026-09-18, which is a month before this writing and after the 1.0 tag, so the tree is ahead of the published version. The repository layout is conventional for a modern TypeScript package, with src, test, scripts, a tsdown config, a vitest config and a CONTRIBUTING file, and the release job is handled by the bumpp script rather than by a manual version bump. Any dependency you take on here is a single package with no transitive tree, which is the main practical argument for accepting the generator syntax in the first place.
Editorial conclusion
quansync fits a library author who needs the same logic exposed synchronously and asynchronously without duplicating it, especially in Node where sync file and child process calls still have a place. It does not fit a codebase that never wants a blocking call, or a browser bundle where shipping both paths is pointless. Before adopting it, check that your build can handle generator functions and, if you want the async/await form, that your bundler can run unplugin-quansync, and note that the benchmark command in the README measures the generator overhead rather than your own workload.
Frequently asked questions
What does quansync do?
It lets you write one implementation and expose it with both a synchronous and an asynchronous interface. The README describes it as a superposition between sync and async, heavily inspired by genasync, with typesafe types, ESM output and zero dependencies.
How do I install quansync and use it?
Install it with pnpm i quansync, then wrap either a pair of sync and async implementations or a generator function with quansync. The resulting object exposes a .sync member and an .async member, and other quansync functions are called from inside the generator with yield*.
Can I use quansync without generator functions?
Yes. A build-time macro in a separate package, unplugin-quansync, lets you write quansync with async and await syntax while still producing the sync version, and the macro entry point is imported from quansync/macro. It requires a build setup that can run the unplugin.
How does quansync handle concurrent calls?
The all helper takes an array of quansync calls and returns their results, written with yield* so it works in both modes. The README example loads three files that way and shows the same function being called with .sync and with .async.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/quansync-dev-quansync)Was this useful?