惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
C
CERT Recently Published Vulnerability Notes
O
OpenAI News
V
Vulnerabilities – Threatpost
C
Cybersecurity and Infrastructure Security Agency CISA
S
Schneier on Security
Latest news
Latest news
F
Full Disclosure
T
Tenable Blog
T
Troy Hunt's Blog
The Last Watchdog
The Last Watchdog
S
Secure Thoughts
L
LangChain Blog
有赞技术团队
有赞技术团队
Project Zero
Project Zero
Cloudbric
Cloudbric
爱范儿
爱范儿
GbyAI
GbyAI
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
The Exploit Database - CXSecurity.com
S
Security @ Cisco Blogs
Hugging Face - Blog
Hugging Face - Blog
Recorded Future
Recorded Future
大猫的无限游戏
大猫的无限游戏
Last Week in AI
Last Week in AI
C
Cisco Blogs
WordPress大学
WordPress大学
Apple Machine Learning Research
Apple Machine Learning Research
小众软件
小众软件
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V2EX - 技术
V2EX - 技术
Engineering at Meta
Engineering at Meta
Spread Privacy
Spread Privacy
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Hacker News: Ask HN
Hacker News: Ask HN
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Schneier on Security
Schneier on Security
T
Threat Research - Cisco Blogs
M
MIT News - Artificial intelligence
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
K
Kaspersky official blog
The Hacker News
The Hacker News
V
V2EX
F
Fortinet All Blogs
L
LINUX DO - 最新话题
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
News | PayPal Newsroom
博客园 - 三生石上(FineUI控件)
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org

Anthony Fu

Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony's Roads to Open Source - The Set Theory (React ver.) Mental Health in Open Source The Evolution of Shiki v1.0 The Magic in Shiki Magic Move Anthony's Roads to Open Source - The Progressive Path Anthony Fu Anthony Fu Anthony Fu Anthony's Roads to Open Source - The Set Theory Now, and the Future of Nuxt Devtools Anthony's Roads to Open Source - The Set Theory Anthony Fu Anthony Fu Stable Diffusion QR Code 101 Refining AI Generated QR Code Stylistic QR Code with Stable Diffusion Anthony Fu How I Manage GitHub Notifications Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Dev SSR on Nuxt with Vite Why I don't use Prettier Anthony Fu Anthony Fu Ship ESM & CJS in one Package Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Reflection of Speaking in Public Anthony Fu Windi CSS and Tailwind JIT Typed Provide and Inject in Vue Color Scheme for VS Code Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Destructuring... with object or array? Anthony Fu Anthony Fu Make Libraries Working with Vue 2 and 3 Anthony Fu Anthony Fu Anthony Fu Anthony Fu
Anthony Fu
Anthony Fu · 2025-03-03 · via Anthony Fu

The Coloring Problem

In modern programming, the function coloring problem isn’t new. Based on how functions execute: synchronous (blocking) and asynchronous (non-blocking), we often classify them into two "colors" for better distinction. The problem arises because you generally cannot mix and match these colors freely.

For instance, in JavaScript:

  • An async function can call both sync and other async functions.
  • A sync function, however, cannot directly call an async function without changing its own color to async.

This restriction forces developers to propagate the "color" throughout their codebase. If a function deep in your logic needs to become async, it forces every caller up the chain to also become async, leading to a cascading effect (or "async inflection"). This makes refactoring harder, increases complexity, and sometimes leads to awkward workarounds like blocking async calls with await inside sync contexts, or vice versa.

build()transform()parse()loadFile()replace()await build()await transform()await parse()await loadFile()replace()

If `loadFile()` needs to be async, then all its callers upstream need to change to async too.

We often discuss the async inflection problem, where a common solution is to make everything async since async functions can call both sync and async functions, while the reverse is not true. However, the coloring problem actually goes both ways, which seems to be less frequently discussed:

While an async function requires all the callers to be async, a sync function also requires all the dependencies to be sync.

await build()await transform()await parse()await loadFile()replace()await build()await transform()parseSync()loadFileSync()replace()

If `parse()` needs to be sync, then all its dependencies down the road need to be sync too.

At its core, it’s the same problem with different perspectives. It depends on which part of the code you’re focusing on and how difficult it is to change its "color." If the function you’re working on must be async, the burden shifts to the callers. Conversely, if it must be sync, you’ll need all your dependencies to be sync or provide a synchronous entry point.

Libraries in Practice

For example, the widely used library find-up provides two main APIs, findUp and findUpSync, to avoid dependents being trapped by the coloring problem. If you look into the code, you’ll find that the package essentially duplicates the logic twice to provide the two APIs. Going down, you see its dependency locate-path also duplicates the locatePath and locatePathSync logic.

Say you want to build another library that uses findUp, like readNearestPkg, you would also have to write the logic twice, using findUp and findUpSync separately, to support both async and sync usage.

In these cases, even if our main logic does not come with its own "colors," the whole dependency pipeline is forced to branch into two colors due to an optional async operation down the road (e.g., fs.promises.stat and fs.statSync).

[async consumers]readNearestPkg()findUp()locatePath()fs.stat()[sync consumers]readNearestPkgSync()findUpSync()locatePathSync()fs.statSync()my-pkgfind-uplocate-path

Basically, we would maintain two branches of code to support both sync and async, with only a few sync utils that can be shared.

Async Plugins

Another case demonstrating the coloring problem is a plugin system with async hooks. For example, imagine we are building a Markdown-to-HTML compiler with plugin support. Say the parser and compiler logic are synchronous; we could expose a sync API like:

export function markdownToHtml(markdown) {
  const ast = parse(markdown)
  // ...
  return render(ast)
}

To make our library extensible, we might allow plugins to register hooks at multiple stages thoughout the process, for example:

export interface Plugin {
  preprocess: (markdown: string) => string
  transform: (ast: AST) => AST
  postprocess: (html: string) => string
}

export function markdownToHtml(markdown, plugins) {
  for (const plugin of plugins) {
    markdown = plugin.preprocess(markdown)
  }
  let ast = parse(markdown)
  for (const plugin of plugins) {
    ast = plugin.transform(ast)
  }
  let html = render(ast)
  for (const plugin of plugins) {
    html = plugin.postprocess(html)
  }
  return html
}

Great, now we have a plugin system. However, having markdownToHtml as a synchronous function essentially limits all plugin hooks to be synchronous as well. This limitation can be quite restrictive. For instance, consider a plugin for syntax highlighting. In many cases, the best results for syntax highlighting might require asynchronous operations, such as fetching additional resources or performing complex computations that are better suited for non-blocking execution.

To accommodate such scenarios, we need to allow async hooks in our plugin system. This means that our main function, markdownToHtml, as the caller of the plugin hooks must also be async. We could implement it like this:

export interface Plugin {
  preprocess: (markdown: string) => string | Promise<string>
  transform: (ast: AST) => AST | Promise<AST>
  postprocess: (html: string) => string | Promise<string>
}

export async function markdownToHtml(markdown, plugins) {
  for (const plugin of plugins) {
    markdown = await plugin.preprocess(markdown)
  }
  let ast = parse(markdown)
  for (const plugin of plugins) {
    ast = await plugin.transform(ast)
  }
  let html = render(ast)
  for (const plugin of plugins) {
    html = await plugin.postprocess(html)
  }
  return html
}

While this maximized the flexibility of the plugin system, this approach also forces all users to handle the process asynchronously, even in the cases where all plugins are synchronous. This is the cost of accommodating the possibility that some operations "might be asynchronous". To manage this, we often end up duplicating the logic to offer both sync and async APIs, and restrict async plugins to the async version only.

Such duplications lead to increased maintenance efforts, potential inconsistencies, and larger bundle sizes, which are not ideal for maintainers or users.

Is there a better way to handle this?

Introducing Quansync

What if we could make our logic decoupled from the coloring problem and let the caller decide the color?

Trying to make the situation a bit better, SXZZ and I took inspiration from gensync by LOGANFSMYTH and made a package called quansync. Taking it even further, we are dreaming of leveraging this to create a paradigm shift in the way we write libraries in the JavaScript ecosystem.

The name Quansync is borrowed from Quantum Mechanics, where particles can exist in multiple states simultaneously, known as superposition, and only settle into a single state when observed (try hovering over the atom below).

Quantum + aSync

"Superposition"
between sync and async

You can think of quansync as a new type of function that can be used as both sync and async depending on the context. In many cases, our logic can escape the async inflection problem, especially when designing shared logic with optional async hooks.

[async consumers][sync consumers]*readNearestPkg()*findUp()*locatePath()*stat()fs.stat()fs.statSync()

Try hovering over either the async or sync side.
Quansync functions are in purple, which can adapt to either sync or async.

Usage Examples

Quansync provides a single API with two overloads.

Wrapper API

Wrapper allows you to create a quansync function by providing a sync and an async implementation. For example:

import fs from 'find-up'
import { quansync } from 'quansync'

export const readFile = quansync({
  sync: filepath => fs.readFileSync(filepath),
  async: filepath => fs.promises.readFile(filepath),
})
const content1 = readFile.sync('package.json')
const content2 = await readFile.async('package.json')

// The quansync function itself can behave like a normal async function
const content3 = await readFile('package.json')

Generator API

Generator is where the magic happens. It allows you to create a quansync function by using other quansync functions. For example:

import { quansync } from 'quansync'

export const readFile = quansync({
  sync: filepath => fs.readFileSync(filepath),
  async: filepath => fs.promises.readFile(filepath),
})

// Create a quansync with `function*` and `yield*`
export const readJSON = quansync(function* (filepath) {
  // Call the quansync function directly
  // and use `yield*` to get the result.
  // Upon usage, it will auto select the implementation
  const content = yield* readFile(filepath)
  return JSON.parse(content)
})
// fs.readFileSync will be used under the hood
const pkg1 = readJSON.sync('package.json')
// fs.promises.readFile will be used under the hood
const pkg2 = await readJSON.async('package.json')

Build-time Macros

If the function* and yield* syntax scares you a bit, SXZZ also made a build-time macro unplugin-quansync allowing you to write normal async/await syntax, and it will be transformed to the corresponding yield* syntax at build time.

import { quansync } from 'quansync/macro'

// Use async/await syntax
// They will be transformed to `function*` and `yield*` at build time
export const readJSON = quansync(async (filepath) => {
  const content = await readFile(filepath)
  return JSON.parse(content)
})

// Expose the classical sync API
export const readJSONSync = readJSON.sync

Thanks to unplugin, it can work in almost any build tool, like compiling with unbuild or testing with vitest. Please refer to the docs for more detailed setup.

How does it Work?

Generators in JavaScript are a powerful yet often underutilized feature. To define a generator function, you use the function* syntax (note that arrow functions do not support generators). Inside a generator function, you can use the yield keyword to pause execution and return a value. This effectively splits your logic into multiple "chunks," allowing the caller to control when to execute the next chunk.

By leveraging this behavior, we can pause execution at each yield point. In an asynchronous context, we can wait for the async operation to complete before resuming execution. In a synchronous context, the next chunk runs immediately. This approach offloads the coloring problem to the caller, allowing them to decide whether the function should run synchronously or asynchronously.

In fact, during the early days of JavaScript, before the async and await keywords were widely adopted, Babel used generators and yield to polyfill async behavior. While this technique isn’t new, we believe it has significant potential to improve how we handle the coloring problem, especially in library design.

When not to Use?

Frankly, I wish most of time you don’t even need to think about it. High-level tools should support async entry points for most cases, where choice sync and async is not a problem. However, there are still many cases where in the context, it’s required to be colored. In such cases, quansync could be a good fit for progressive and gradual adoption.

Promise in JavaScript naturally a microtask that delays a tick. yield also introduce certain overhead (around ~120ns on M1 Max). In performance-sensitive scenarios, you might also want to avoid using either asyncor quansync.

Coloring Problem Revisited

While quansync doesn’t completely solve the coloring problem, it provides a new perspective that simplifies managing synchronous and asynchronous code. Quansync introduces a new "purple" color, blending the red and blue. Quansync functions still face the coloring problem, as wrapping a function to support both sync and async requires it to be a quansync function (or generator). However, the key advantage is that a quansync function can be "collapsed" to either sync or async as needed. This allows your "colorless" logic to avoid the red and blue color inflection caused by some operations that might have a color.

Conclusion

This is a new approach to tackling the coloring problem we are still exploring. We will slowly roll out quansync in our libraries and see how it improves our experience and the ecosystem. We are also looking for feedback and contributions, so feel free to join us in the Discord or GitHub Discussions to share your thoughts.