Troubleshooting

Symptoms, causes and fixes for the problems people hit most often with signals, commands, collections, threads and compilation.

Find the symptom, check the cause, apply the fix. Most entries come from three facts: subscriptions end when their handle is dropped, streams are cold and delivery is synchronous, and collections only know what a source or a refresh tells them.

#Nothing is delivered

SymptomCauseFix
A subscriber never runsThe Subscription was dropped: stream.subscribe(f)?; ends at the semicolon, and binding to _ drops immediatelyBind it: let _subscription = ..., store it in a field, or give it to a DisposeBag with dispose_with(&bag)
A derived ObservableList stays emptyThe wrapper (and its populating subscription) was droppedKeep the wrapper alive; clones share one subscription
A Computed stopped updatingThe Computed was dropped; its ReadSignal keeps the last valueStore the Computed in the struct that owns the derived state
Work happens before the view appears, or neverActivation: work registered with when_activated runs only while a token is aliveBind the ActivationToken (let token = vm.activator().activate()?;); an unbound token deactivates at once
A late subscriber misses a notificationThe state is carried by an EventBus, which does not replayMake it a Signal, or subscribe before the event is emitted

#Missing initial values

SymptomCauseFix
No initial value from a signalSubscribed to changes()Use values()
Two initial values from a signalvalues().start_with(x)Drop the start_with; values() already emits the current value
connect() emits nothing for an empty sourceAn empty source sends no snapshotstart_with_empty() for an initial empty batch
count() never reports 0count() emits only when the count changescount().start_with(0), or is_empty() / is_not_empty(), which always emit one initial value
Computed::from_stream shows the seed foreverThe stream has not emitted (empty source aggregates stay silent)Pass a sensible initial
skip_initial() swallowed the first real changeOn an empty source it drops the first non-empty batchUse changes() when the source may be empty
A dynamic filter or sort shows nothingNo item passes until the first state, predicate or comparer arrivesDrive it from signal.values(), which emits at once, not changes()

#Views that do not update

SymptomCauseFix
A filter, sort or group ignores a field that changed inside an itemItems changed in place; nothing told the pipelinerefresh_key / refresh_items, or auto_refresh(|item, key| item.field.changes())
Every item refreshes once at startupauto_refresh with Signal::values() (its initial emission counts)Use changes() for refresh triggers
Refreshes have no effectsuppress_refresh() upstream, or transform (not transform_on_refresh) forwards the old projectionRemove the suppression; use transform_on_refresh
Rows lose selection or scroll position on every pollThe view is reloaded with load (a Clear plus re-add)Use edit_diff / edit_diff_by
Rows are identified by position and break when sortedKeys were dropped early (sort_by_key on a cache returns a list)Use sort_keyed_* and key widgets by the stream key
A custom binding ends up out of syncChanges were applied out of order, or an Update with a new index was treated as in-placeApply changes front to back; previous_index != current_index means remove then insert
Paging shows an empty page after a searchThe page stayed on a page beyond the narrowed result (page is zero-based and not clamped)Reset the page when the search changes
change_key followed by a sort gives odd orderchange_key emits no indexesSort after change_key

#Errors returned and panics

SymptomCauseFix
CollectionError::NestedUpdateA source was edited, read, connected or watched from inside its own edit, key selector or T::clone on the same threadUse the updater’s read methods (lookup, get, items) inside the edit, or move the access outside it
CollectionError::Contendedwasm32 only: another thread holds the sourceRetry on the next frame, or give the source a single writer
CollectionError::Poisoned foreverA panic inside an edit closure (or key selector) poisoned the sourceKeep edit closures panic-free; recreate the source
CommandError::Disabledcan_execute is false, or ExecutionPolicy::IgnoreWhileRunning refused an overlapping callCheck can_execute(); this is not a failure of the action
A panic from top(0), top_keyed(0) or limit_size_to(0)Zero is rejected with an assertionValidate user-provided sizes first
An error matched with no wildcard arm fails to compileReactiveError, CollectionError and CommandError are #[non_exhaustive]Add a _ arm

#Hangs and loops

SymptomCauseFix
A test or app hangs after a writeA subscriber writes back into what it observes unconditionally (add_or_update publishes even for equal values)Use add_or_update_if_changed, set (not replace), or check the incoming change
Deadlock inside Signal::readThe closure writes to the same signalRead, finish, then write; writing to other signals is fine
Deadlock between two threads editing two sourcesEach touches the other source from inside its edit closureAccess other sources in one global order, or outside the edit
Deadlock in a subscriberA writer publishes while holding a lock the subscriber also takesNever publish while holding such a lock
Memory never freedA subscriber captured an Arc of the struct that owns the subscription (a cycle)Capture the individual signals or sources, or a Weak

#Threads and platforms

SymptomCauseFix
UI toolkit complains about off-thread accessSubscribers run on the writer’s thread, or on a timer or worker threadSend changes through a channel and apply them on the UI thread (Application patterns)
transform_async hangs or panics inside a Tokio callFutures run on a private worker with block_on, outside your runtimeUse runtime-agnostic futures, or spawn on your runtime and await the join handle
wasm32 panics on thread::spawn or Instant::nowA thread-backed operator or the default schedulerUse a *_with_scheduler variant with your own scheduler; avoid transform_async*, async_dispose_many* and collect_update_stats
Browser main thread freezes on a signal readget() waits for the lock while a worker writestry_get() / try_read(...), retry next frame
A timed operator never fires in a testThe test uses the real scheduler or never advances the ManualSchedulerPass a ManualScheduler and call advance_by

#Compile errors

ErrorCauseFix
ValueStream / Result is ambiguousBoth preludes are glob-imported and the bare name is usedWrite dxn_reactive::ValueStream or dxn_reactive::collections::Result, or import specific items
Result takes one generic argumentA prelude shadows std::result::ResultWrite std::result::Result<T, E> where you need two parameters
Trait bound PartialEq / Clone / Send + Sync + 'static not satisfiedItems need Clone + Send + Sync + 'static; sorts, pages and edit_diff also need PartialEqDerive them, or wrap large or non-comparable payloads in Arc with a manual PartialEq
Type annotations needed on ValueStream::newThe error type is no longer inferred from a scalar operatorValueStream::<T, ReactiveError>::new, ValueStream::from_reactive, or the collections::ValueStream alias
? cannot convert a collection error inside when_activatedThe callback returns ReactiveErrorCollectionError converts into it; make sure the closure returns dxn_reactive::Result<()>
Signal<SourceList<T>>::set does not compileset needs PartialEq, which sources lackUse replace
unused_must_use on a subscriptionThe handle is droppedBind and own it

#Still stuck

Reduce the problem to a recorder: subscribe to the pipeline stage you suspect with a record_batches helper (see Testing) and compare the exact change sets with what the collection model says each operator should emit. With the diagnostics feature, collect_update_stats() shows how many adds, updates, removes, refreshes and moves a stage produced.

Next: Coming from DynamicData and ReactiveUI.