GraphiteEditor/Graphite · critical
Failed to send response
Error message
Failed to send response
What it means
InternalNodeGraphUpdateSender::send_compilation_response pushes a CompilationResponse from the NodeRuntime worker thread back to the main thread over an mpsc channel whose receiver lives in NodeRuntimeIO. The expect fires when that receiver has been dropped — the main-thread side was destroyed (application teardown, executor dropped, or runtime replaced) while the worker was still finishing a compilation. Because this runs on the worker thread, the panic kills the runtime thread; the main thread then typically crashes later on its next request send with the 'Failed to send generation request' error, making these two panics a coupled symptom pair.
Source
Thrown at editor/src/node_graph_executor/runtime.rs:101
}
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ExportConfig {
pub name: String,
pub file_type: FileType,
pub scale_factor: f64,
pub bounds: ExportBounds,
pub size: UVec2,
pub artboard_name: Option<String>,
pub artboard_count: usize,
}
#[derive(Clone)]
struct InternalNodeGraphUpdateSender(Sender<NodeGraphUpdate>);
impl InternalNodeGraphUpdateSender {
fn send_compilation_response(&self, response: CompilationResponse) {
self.0.send(NodeGraphUpdate::CompilationResponse(response)).expect("Failed to send response")
}
fn send_execution_response(&self, response: ExecutionResponse) {
self.0.send(NodeGraphUpdate::ExecutionResponse(Box::new(response))).expect("Failed to send response")
}
fn send_eyedropper_preview(&self, raster: Raster<CPU>) {
self.0.send(NodeGraphUpdate::EyedropperPreview(raster)).expect("Failed to send response")
}
}
impl NodeGraphUpdateSender for InternalNodeGraphUpdateSender {
fn send(&self, message: NodeGraphUpdateMessage) {
self.0.send(NodeGraphUpdate::NodeGraphUpdateMessage(message)).expect("Failed to send response")
}
}
// TODO: Replace with `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>) or similarView on GitHub (pinned to c507b35645)
Solutions
- Replace the expect with a checked send: on Err, log 'node graph update receiver dropped' and stop processing (the channel is permanently closed).
- Fix the lifecycle race: keep the NodeRuntimeIO receiver alive as long as the runtime thread can send (join the thread before dropping IO, or use an Arc-kept channel).
- On shutdown, signal the runtime thread to drain and exit before the receiver is dropped (send a Shutdown request and join).
- When replacing the runtime (replace_node_runtime), stop the old thread first so in-flight responses have a live receiver.
Example fix
// before
fn send_compilation_response(&self, response: CompilationResponse) {
self.0.send(NodeGraphUpdate::CompilationResponse(response)).expect("Failed to send response")
}
// after
fn send_compilation_response(&self, response: CompilationResponse) {
if self.0.send(NodeGraphUpdate::CompilationResponse(response)).is_err() {
log::warn!("node graph receiver dropped; discarding compilation response");
}
} Defensive patterns
Strategy: fallback
Validate before calling
// On the worker: probe cheaply is not possible with std mpsc, so make send tolerant
fn send_compilation_response(&self, response: CompilationResponse) {
if self.0.send(NodeGraphUpdate::CompilationResponse(response)).is_err() {
// consumer dropped (shutdown): stop emitting further updates
}
} Try / catch
// Wrap worker sends so a closed channel unwinds the task instead of killing the thread:
let Ok(()) = self.0.send(NodeGraphUpdate::CompilationResponse(response)) else {
log::warn!("receiver dropped; ending runtime send loop");
return;
}; Prevention
- Design shutdown as an explicit handshake (Stop message + thread join) instead of dropping the receiver and hoping the worker notices.
- Keep the receiver alive for the runtime thread's lifetime when late responses are legitimate.
- Never expect on sends from background threads — a panic there is silent to the UI.
When it happens
Trigger: The NodeRuntime finishes compiling a document graph and calls send_compilation_response after the main-thread Receiver<NodeGraphUpdate> inside NodeRuntimeIO was dropped (app exiting, executor replaced via replace_node_runtime, or document close tearing down the IO).
Common situations: Shutdown races: quit during an active compilation; re-initializing the node runtime dropping the old channel pair while a compilation is in flight; tests spawning short-lived runtimes that compile past the receiver's drop; a panic on the main thread unwinding and dropping receivers mid-compilation.
Related errors
- Failed to send generation request
- Failed to send editor preferences
- Star node can't be found
- Brush node does not exist
- Path node does not exist
AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16).
Data as JSON: /api/errors/924181f5831de89d.
Report an issue: GitHub.