FuelLabs/fuel-core · error · anyhow::Error
Stream closed without transaction status
Error message
Stream closed without transaction status
What it means
Thrown by QueryService::submit_and_await_commit (crates/fuel-core/src/service/query.rs:57). The node subscribes to status updates for the transaction id, submits the tx, then awaits the next final status on the filtered stream. The stream returning None means the status-update channel closed before any final status was delivered — in practice the tx status manager or the node itself was dropped or shut down mid-wait.
Source
Thrown at crates/fuel-core/src/service/query.rs:76
&self,
tx: Transaction,
) -> anyhow::Result<TransactionStatus> {
let id = tx.id(&self
.shared
.config
.snapshot_reader
.chain_config()
.consensus_parameters
.chain_id());
let stream = self.transaction_status_change(id).await?.filter(|status| {
futures::future::ready(status.as_ref().is_ok_and(|status| status.is_final()))
});
futures::pin_mut!(stream);
self.submit(tx).await?;
stream
.next()
.await
.ok_or_else(|| anyhow::anyhow!("Stream closed without transaction status"))?
}
/// Return a stream of status changes for a transaction.
pub async fn transaction_status_change(
&self,
id: Bytes32,
) -> anyhow::Result<impl Stream<Item = anyhow::Result<TransactionStatus>> + '_> {
// First subscribe to the statuses, and only after that create a view.
let tx_status_manager = &self.shared.tx_status_manager;
let rx = tx_status_manager.tx_update_subscribe(id).await?;
let db = self.shared.database.off_chain().latest_view()?;
let state = StatusChangeState {
db,
tx_status_manager,
};
Ok(transaction_status_change(state, rx, id, true).await)
}
}View on GitHub (pinned to b9d4d170da)
Solutions
- Check node liveness and logs first — a shutdown or panic during the await is the usual cause; restart the node.
- Retry the submission after restart: submitting the same signed transaction is idempotent (same tx id), then await the status again.
- Use transaction_status_change (the raw stream API) and handle closure explicitly when you need custom retry logic instead of submit_and_await_commit.
- Wrap the await in a client-side timeout so node shutdowns surface as timeouts rather than dangling awaits.
Example fix
// before: single shot, fails when the node restarts mid-await
let status = client.submit_and_await_commit(tx).await?;
// after: retry on stream closure (same signed tx -> same id, safe to resubmit)
loop {
match client.submit_and_await_commit(tx.clone()).await {
Ok(status) => break status,
Err(e) if e.to_string().contains("Stream closed") => {
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
Err(e) => return Err(e),
}
} Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight liveness check before submitting
async fn node_alive(client: &fuel_client::client::FuelClient) -> bool {
client.health().await.is_ok()
}
if !node_alive(&client).await {
return Err(anyhow::anyhow!("node unreachable before submit"));
} Try / catch
loop {
match client.submit_and_await_commit(tx.clone()).await {
Ok(status) => break Ok(status),
Err(e) if e.to_string().contains("Stream closed without transaction status") => {
// node likely restarted; wait, verify health, then resubmit the same signed tx
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
Err(e) => break Err(e),
}
} Prevention
- Do not stop or restart the node between submit and final status when using submit_and_await_commit.
- Wrap the await in a client-side timeout so shutdowns surface deterministically.
- Prefer the transaction_status_change stream when you need explicit control over channel closure.
- Resubmitting the same signed transaction is safe: it deduplicates by tx id.
When it happens
Trigger: The node shuts down (SharedState or tx_status_manager dropped) while the caller is awaiting commit; a restart lands between submission and final status; abnormal service teardown during heavy load.
Common situations: Automation that stops the node right after submitting a transaction; container or pod restarts mid-await; clients holding the future across process boundaries; changes in status manager lifecycle across fuel-core versions.
Related errors
- Not implemented yet
- Missing code for contract: {id}
- Missing utxo for contract: {id}
- Missing column. Check the schema!
- No block height found
AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16).
Data as JSON: /api/errors/81ec63b5c0bc22ae.
Report an issue: GitHub.