libnyanpasu/clash-nyanpasu · error
failed to join notify task: {error}
Error message
failed to join notify task: {error} What it means
Raised as an AckStatus::Failed entry by the parallel notify executor when `JoinSet::join_next()` returns a JoinError — i.e. a spawned subscriber-notification task panicked or was cancelled and its result could not be joined. The library synthesizes a synthetic failed ack named "<notify task join failure>" so the transaction still records that a required subscriber notification did not complete, rather than silently losing the notification.
Source
Thrown at backend/nyanpasu-core/src/state/transaction/notify.rs:192
let subscriber = Arc::clone(subscriber);
join_set.spawn(async move { (index, Self::notify_one(&change, subscriber).await) });
}
let mut acks = Vec::new();
while let Some(res) = join_set.join_next().await {
match res {
Ok((index, ack)) => acks.push((index, ack)),
Err(error) => {
tracing::error!("failed to join notify task: {error}");
acks.push((
usize::MAX,
SubscriberAck {
name: SubscriberName(Cow::Borrowed("<notify task join failure>")),
policy: AckPolicy::Required,
timeout: Duration::from_secs(0),
elapsed: Duration::from_secs(0),
status: AckStatus::Failed {
error: anyhow::anyhow!("failed to join notify task: {error}")
.into(),
},
},
));
}
}
}
acks.sort_by_key(|&(index, _)| index);
acks.into_iter().map(|(_, ack)| ack).collect()
}
}
impl<T> NotifyExecutor<T, Prepared, Sequential>
where
T: Clone + Send + Sync + 'static,
{
pub async fn notify_all(
change: &StateChange<T>,View on GitHub (pinned to f7dbce2997)
Solutions
- Find the subscriber whose task panicked: the JoinError carries the panic message; fix the panic in that subscriber's notification handler.
- Harden subscriber callbacks against unexpected state values (no unwrap/expect on state payloads; validate before use).
- Check for runtime shutdown/abort calls that could cancel in-flight notify tasks and ensure transactions complete before shutdown.
- Treat the resulting transaction result as degraded — required acks failed — and re-notify or resubscribe the affected subscriber.
Example fix
// before: panicking subscriber
fn on_change(state: &Config) { let v = state.tun.as_ref().unwrap(); }
// after: defensive subscriber
fn on_change(state: &Config) {
let Some(v) = state.tun.as_ref() else { tracing::warn!("tun missing"); return };
}
Defensive patterns
Strategy: try-catch
Validate before calling
// smoke-test subscribers before subscribing them to real transactions
fn validate_subscriber<T>(sub: &ArcStateSubscriber<T>, sample: &StateChange<T>) {
assert!(tokio::spawn(sub.notify(sample)).await.is_ok(), "subscriber task panicked");
} Type guard
fn is_join_failure(ack: &SubscriberAck) -> bool {
matches!(&ack.status, AckStatus::Failed { .. })
&& ack.name.0.contains("notify task join failure")
} Try / catch
let acks = NotifyExecutor::notify_all(&change, &subs).await;
for ack in acks {
if let AckStatus::Failed { error } = &ack.status {
if ack.name.0.contains("notify task join failure") {
tracing::error!("subscriber task panicked: {error:#}");
// reschedule notification for the affected subscriber
}
}
} Prevention
- Never panic in subscriber notification handlers; return Result instead of unwrap/expect.
- Fuzz/panic-test subscriber callbacks with unusual state values before wiring them into transactions.
- Avoid aborting notification tasks; let transactions drain before runtime shutdown.
- Treat required-ack join failures as degraded transactions and re-notify rather than ignoring the ack.
When it happens
Trigger: A subscriber notification task spawned inside `NotifyExecutor::notify_all` panics or is aborted, causing `join_set.join_next()` to yield Err(error); the synthetic ack is pushed with this message.
Common situations: A subscriber callback panics on the new state value (e.g. unwrapping None, index panic); a notification task is aborted by runtime shutdown; a bug in a subscriber's async handler causes task cancellation.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- write config timed out after {timeout:?}
- state transaction dropped before commit or rollback complete
- Failed to wait for widget process: {}
- failed to create tokio runtime
- unimplemented!()
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/c72f0df933b818ff.
Report an issue: GitHub.