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.
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.
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.
- ✅ 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
npm install async-immerHolds:
- The current state
- A monotonically increasing version
const container = new StateContainer({ count: 0 });The version is what protects you from stale async commits.
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
import {StateContainer , asyncProduce ,immerEngine} from 'async-immer'
await asyncProduce(container, draft => {
draft.count += 1;
}, immerEngine);
console.log(container.state);
// { count: 1 }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.
fetchSlow().then(data => setState({ value: data }));
fetchFast().then(data => setState({ value: data }));If fetchSlow finishes last → stale overwrite.
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.
Every call returns a result:
const result = await asyncProduce(...);
if (result.status === "aborted") {
// stale or failed update
}No silent corruption. No undefined behavior.
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.
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
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
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.
If Immer made immutability ergonomic,
async-immer makes it safe in the real world.
Built on top of Immer.
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.