dotnet/reactive · error · InvalidOperationException

Strings_Core.REENTRANCY_DETECTED

Error message

Strings_Core.REENTRANCY_DETECTED

What it means

CheckedObserver is a diagnostic wrapper that detects reentrant notifications. CheckAccess uses an Interlocked.CompareExchange state machine (Idle/Busy/Done); if a callback (OnNext/OnError/OnCompleted) is invoked while the same observer is already inside a notification (state Busy), it throws InvalidOperationException(REENTRANCY_DETECTED).

Solutions

  1. Restructure the notification graph so the observer never re-enters itself (break the cycle, defer with ObserveOn or a Scheduler).
  2. Ensure notifications are serialized to one thread at a time (e.g. ObserveOn a single-threaded context) if concurrency is the cause.
  3. Remove or replace the CheckedObserver wrapper if you only needed it for debugging.

Example fix

// before
source.Subscribe(x => subject.OnNext(x)); // subject feeds back into same observer
// after
source.ObserveOn(TaskPoolScheduler.Default).Subscribe(x => { if (x != last) subject.OnNext(x); });
Defensive patterns

Strategy: try-catch

Validate before calling

// track in-flight state yourself before re-notifying
private int _inFlight;
bool CanNotify() => Interlocked.CompareExchange(ref _inFlight, 1, 0) == 0;

Type guard

bool IsReentrantCall() => _insideNotification; // set true at callback entry, false at exit

Try / catch

try { observer.OnNext(value); } catch (InvalidOperationException ex) when (ex.Message.Contains("Reentrancy")) { /* defer via scheduler or break the cycle */ }

Prevention

When it happens

Trigger: Calling OnNext/OnError/OnCompleted on the observer from within one of its own callbacks, or concurrently from another thread while a notification is in flight. Typically occurs when using Observable.Create with a CheckedObserver in CreateCheckedObserver-based tests or instrumented pipelines.

Common situations: Subjects/subscribers whose OnNext re-triggers the same observer (circular reactive graphs); shared observers called from multiple threads; debug/instrumentation builds where CheckedObserver wraps production observers.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of dotnet/reactive@94b5d5ab91 (2026-09-15). Data as JSON: /api/errors/42c4c7fc0ae1a567. Report an issue: GitHub.

Appendix: source

Thrown at Rx.NET/Source/src/System.Reactive/Internal/CheckedObserver.cs:70

        {
            CheckAccess();

            try
            {
                _observer.OnCompleted();
            }
            finally
            {
                Interlocked.Exchange(ref _state, Done);
            }
        }

        private void CheckAccess()
        {
            switch (Interlocked.CompareExchange(ref _state, Busy, Idle))
            {
                case Busy:
                    throw new InvalidOperationException(Strings_Core.REENTRANCY_DETECTED);
                case Done:
                    throw new InvalidOperationException(Strings_Core.OBSERVER_TERMINATED);
            }
        }
    }
}

View on GitHub (pinned to 94b5d5ab91)