R3: The Rx Rewrite That Says the Old Rules Were Wrong

Cysharp’s modern reactive library keeps the LINQ feel of Rx, but swaps in a new execution model for .NET 8, game loops, and low-allocation event streams.

9 min read • View on GitHub • More from Cysharp

A mechanical relay line carries events from left to right. A small fault sparks at one node, but a thin switch diverts the error into a side channel while the main flow keeps moving toward a UI panel and a frame clock. The image explains R3’s central claim: one exception does not have to kill the whole stream.
R3’s signature move is philosophical as much as technical. It reroutes failure instead of letting one exception collapse the pipeline.

The biggest difference is that in normal Rx, when an exception occurs in the pipeline, it flows to OnError and the subscription is unsubscribed, but in R3, it flows to OnErrorResume and the subscription is not unsubscribed. I consider the automatic unsubscription by OnError to be a bad design for event handling.

Yoshifumi Kawai (neuecc), Creator/CTO of Cysharp · R3 on OpenUPM
Key Takeaways

R3 opens with a provocation: the classic Rx rule that an exception should end a stream is not sacred. In Cysharp’s view, that behavior is a poor fit for UI, games, and any system that is meant to stay alive after one bad event.


The rule R3 breaks on purpose

That single design choice explains a lot of the repo. If a button handler throws, or a game-loop observer fails for one frame, R3 does not assume the whole reactive world is now invalid. It gives you a path to log, recover, and keep moving.

That is the real thesis of the library. R3 keeps the familiar reactive surface, but it changes what happens when the system gets messy. For long-lived interactive software, that is not a corner case. It is the default.

Why classic Rx feels expensive in 2026

The older Rx model carries historical baggage. `IScheduler` is powerful, but it also pushes complexity into every time-based decision. Add allocation-heavy subscription management, and you get a framework that can feel elegant on paper and fussy in real-time systems.

The old model treats time as a generic scheduling problem. R3 treats timing as part of the application’s runtime shape, which is why frame sync matters.

DimensionRx.NETUniRxR3
Error semantics`OnError` ends the subscription`OnError` ends the subscription`OnErrorResume` keeps the stream alive
Timing model`IScheduler`-centricUnity-aware, but inherited old Rx assumptions`TimeProvider` and frame providers
Allocation behaviorHeavier subscriber machineryOptimized for Unity, but legacy-shapedDesigned to reduce allocation pressure
Game engine fitPossible, but not nativeStrong for UnityBuilt for Unity, Godot, Stride, and more
Modern .NET alignmentOlder abstraction setPartialC# 12+, async/await, `ValueTask`, `TimeProvider`
Two clocks sit side by side. One is a system timer with slight drift marks and uneven ticks. The other is a frame clock with clean, repeated beats labeled Update, LateUpdate, and FixedUpdate, with an event chain pinned directly to it. The image explains why R3 treats frames as a native execution context instead of a workaround.
R3’s timing story is not about scheduling anything anywhere. It is about choosing the right clock for the runtime you are actually in.

R3’s core trick: the stream does not have to die

public abstract class Observer<T>
{
    public abstract void OnNext(T value);
    public abstract void OnErrorResume(Exception error);
    public abstract void OnCompleted();
}

public abstract class Observable<T>
{
    public abstract IDisposable Subscribe(Observer<T> observer);
}

The abstract-class design is doing real work here. It gives R3 room to bake in lifecycle behavior and internal optimization that would be awkward to express through interfaces alone. The result is a reactive core that is closer to the runtime than classic Rx tends to be.

That matters because R3 is not trying to win by syntax. It is trying to win by execution model. The `Observer` can absorb an error and keep the subscription alive, which makes the library feel less like a purity test and more like infrastructure.

Built for frames, not just timers

This is where the library becomes obvious to game developers and still interesting to everyone else. R3 treats `FrameProvider` as a native timing source, not a compatibility layer. That makes frame-aligned operators feel like part of the platform instead of a workaround bolted onto it.

The same idea shows up in `TimeProvider`. Instead of leaning on older scheduler abstractions, R3 aligns itself with modern .NET timing primitives. The design is cleaner for tests, easier to reason about, and closer to how current .NET code already wants to talk about time.

A quick mental model

Timer-based reactive code asks, “when should this happen?” Frame-based reactive code asks, “which update cycle should this belong to?” R3 makes that second question feel normal.


The sandbox is the proof

The repo’s `/sandbox` breadth is not decoration. It is the evidence that the same reactive core survives contact with Unity, Godot, Stride, WPF, MAUI, Avalonia, Blazor, and more. Libraries that claim to be universal usually reveal their limits fast. R3 spends a lot of effort proving the opposite.

Stopping the pipeline at OnError is a mistake. IScheduler is the root of poor performance. Frame-based operations, a missing feature in Rx, are especially important in game engines.

Yoshifumi Kawai (neuecc), Creator/CTO of Cysharp · Cysharp/R3 GitHub README

That quote is the whole project in miniature. It explains the error philosophy, the performance stance, and the reason frame-based operators are not treated as niche features.


Who built this, and why it matters

A WSJ hedcut-style portrait of Yoshifumi Kawai, also known as neuecc, rendered in black ink on pure white background. The face is shown as a close-cropped editorial portrait to establish authorship and technical authority behind R3.

R3 comes from Yoshifumi Kawai, better known as neuecc, and from Cysharp’s broader habit of building aggressively practical .NET infrastructure. That background matters because the library’s most opinionated choices are not theoretical. They come from shipping systems that have to perform under real constraints.

LibraryBest atMain limitationWhy R3 changes the conversation
Rx.NETThe canonical reactive modelOlder scheduler and error semanticsR3 keeps the feel but updates the runtime contract
UniRxUnity-friendly reactive codeLegacy architectureR3 keeps the engine fit while modernizing the core
ReactivePropertyMVVM state bindingNarrower scopeR3 absorbs much of the reactive state story natively
DynamicDataReactive collectionsSpecialized domain focusR3 pairs broader reactive primitives with modern platform support