Package Exports
- async-immer
Readme
async-immer
π async-immer
async-immer lets you safely mutate state across async boundaries without race conditions or stale writes.
Immer, but safe for async.
async-immer lets you write mutable-looking code while safely handling async state updates, preventing race conditions and stale writes by design.
If youβve ever wondered:
βWhat happens if two async updates finish out of order?β
This library exists because of that question.
π¨ The Problem
Immer is great β until async enters the picture.
setState(prev =>
produce(prev, async draft => {
draft.user = await fetchUser();
})
);Looks harmless. But now consider:
- Multiple async updates running at the same time
- Slow requests finishing after fast ones
- Older async work overwriting newer state
This leads to:
- β Race conditions
- β Stale writes
- β Heisenbugs in production
Immer cannot protect you here β by design.
π‘ The Solution
async-immer introduces versioned, atomic async updates.
It guarantees that:
- Async mutations commit only if state hasnβt changed
- Older async updates cannot overwrite newer state
- Failed or stale updates are safely aborted
- State updates are atomic
All while keeping the same mental model as Immer.
β¨ Key Features
- β Async-safe immutable updates
- β Stale write detection
- β Race condition prevention
- β Atomic commits
- β Framework-agnostic (React, Redux, Zustand, Node, etc.)
- β Engine-agnostic (Immer today, custom engines tomorrow)
- β Production-grade by default
π¦ Installation
npm install async-immerπ§ Core Concepts
1οΈβ£ StateContainer
Holds:
- The current state
- A monotonically increasing version
const container = new StateContainer({ count: 0 });The version is what protects you from stale async commits.
2οΈβ£ asyncProduce
An async-safe version of produce.
import {StateContainer , asyncProduce ,immerEngine} from 'async-immer'
await asyncProduce(
container,
async draft => {
draft.count += 1;
},
immerEngine
);- You can
awaitinside - Mutations are isolated
- Commit happens only if safe
π Basic Usage
Synchronous update
import {StateContainer , asyncProduce ,immerEngine} from 'async-immer'
await asyncProduce(container, draft => {
draft.count += 1;
}, immerEngine);
console.log(container.state);
// { count: 1 }Asynchronous update
import {StateContainer , asyncProduce ,immerEngine} from 'async-immer'
await asyncProduce(container, async draft => {
draft.user = await fetchUser();
draft.permissions = await fetchPermissions();
}, immerEngine);No race conditions. No stale overwrites.
π§ͺ Why This Matters (Real Example)
Without async-immer β
fetchSlow().then(data => setState({ value: data }));
fetchFast().then(data => setState({ value: data }));If fetchSlow finishes last β stale overwrite.
With async-immer β
import {StateContainer , asyncProduce ,immerEngine} from 'async-immer'
asyncProduce(container, async draft => {
await delay(200);
draft.value = "slow";
}, immerEngine);
asyncProduce(container, async draft => {
draft.value = "fast";
}, immerEngine);Result:
{ value: "fast" }The slow update is automatically aborted.
π§― Error & Stale Handling
Every call returns a result:
const result = await asyncProduce(...);
if (result.status === "aborted") {
// stale or failed update
}No silent corruption. No undefined behavior.
π§© Framework-Agnostic by Design
async-immer has zero framework dependencies apart from immer obviously.
Works with:
- React
- Redux / RTK
- Zustand
- Vue / Pinia
- Node backends
- Real-time systems
- Games / simulations
You control when and how state is read.
π Engine-Agnostic
Immer is just the default engine.
asyncProduce(container, recipe, immerEngine);Future engines may include, MAY BE:
- Custom immutable engines
- Structural sharing strategies
- Domain-specific draft engines
π§ When Should You Use This?
Great fit for:
- Async-heavy applications
- Real-time dashboards
- Collaborative tools
- High-concurrency systems
- Anywhere stale state is unacceptable
Probably overkill for:
- Purely synchronous state
- Simple forms or local UI state
π§ Philosophy
Correctness first. Convenience second.
Async bugs donβt show up in dev. They show up in production.
async-immer exists to make those bugs structurally impossible.
π Final Thought
If Immer made immutability ergonomic,
async-immer makes it safe in the real world.
β€οΈ Acknowledgements
Built on top of Immer.
Authorβs Note
Hi cinfinit here π
This library was born from the realization that async bugs donβt crash your app β they quietly corrupt it. By the time you notice, the cause is already gone. async-immer exists to make those failures explicit, detectable, and impossible to ignore. Correctness shouldnβt be optional just because async is involved.