{"record":{"id":"924181f5831de89d","repo":"GraphiteEditor/Graphite","slug":"failed-to-send-response","errorCode":null,"errorMessage":"Failed to send response","messagePattern":"Failed to send response","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"editor/src/node_graph_executor/runtime.rs","lineNumber":101,"sourceCode":"}\n\n#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct ExportConfig {\n\tpub name: String,\n\tpub file_type: FileType,\n\tpub scale_factor: f64,\n\tpub bounds: ExportBounds,\n\tpub size: UVec2,\n\tpub artboard_name: Option<String>,\n\tpub artboard_count: usize,\n}\n\n#[derive(Clone)]\nstruct InternalNodeGraphUpdateSender(Sender<NodeGraphUpdate>);\n\nimpl InternalNodeGraphUpdateSender {\n\tfn send_compilation_response(&self, response: CompilationResponse) {\n\t\tself.0.send(NodeGraphUpdate::CompilationResponse(response)).expect(\"Failed to send response\")\n\t}\n\n\tfn send_execution_response(&self, response: ExecutionResponse) {\n\t\tself.0.send(NodeGraphUpdate::ExecutionResponse(Box::new(response))).expect(\"Failed to send response\")\n\t}\n\n\tfn send_eyedropper_preview(&self, raster: Raster<CPU>) {\n\t\tself.0.send(NodeGraphUpdate::EyedropperPreview(raster)).expect(\"Failed to send response\")\n\t}\n}\n\nimpl NodeGraphUpdateSender for InternalNodeGraphUpdateSender {\n\tfn send(&self, message: NodeGraphUpdateMessage) {\n\t\tself.0.send(NodeGraphUpdate::NodeGraphUpdateMessage(message)).expect(\"Failed to send response\")\n\t}\n}\n\n// TODO: Replace with `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>) or similar","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/editor/src/node_graph_executor/runtime.rs#L83-L119","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nfn send_compilation_response(&self, response: CompilationResponse) {\n\tself.0.send(NodeGraphUpdate::CompilationResponse(response)).expect(\"Failed to send response\")\n}\n\n// after\nfn send_compilation_response(&self, response: CompilationResponse) {\n\tif self.0.send(NodeGraphUpdate::CompilationResponse(response)).is_err() {\n\t\tlog::warn!(\"node graph receiver dropped; discarding compilation response\");\n\t}\n}","handlingStrategy":"fallback","validationCode":"// On the worker: probe cheaply is not possible with std mpsc, so make send tolerant\nfn send_compilation_response(&self, response: CompilationResponse) {\n\tif self.0.send(NodeGraphUpdate::CompilationResponse(response)).is_err() {\n\t\t// consumer dropped (shutdown): stop emitting further updates\n\t}\n}","typeGuard":null,"tryCatchPattern":"// Wrap worker sends so a closed channel unwinds the task instead of killing the thread:\nlet Ok(()) = self.0.send(NodeGraphUpdate::CompilationResponse(response)) else {\n\tlog::warn!(\"receiver dropped; ending runtime send loop\");\n\treturn;\n};","preventionTips":["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."],"tags":["rust","graphite-editor","mpsc","channel-closed","expect-panic","node-runtime","worker-thread"],"backgroundTag":"mpsc-channel-closed","analyzedSha":"c507b356453361e31638b8bff8f6d46b6da2961e","analyzedAt":"2026-08-16T21:57:18.596Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}