GraphiteEditor/Graphite · error

The CEF control thread ended without a result

Error message

The CEF control thread ended without a result

What it means

Panic when result_receiver.recv() returns Err after the CEF message loop shuts down. The control thread is expected to send exactly one result over result_sender before ending; recv() fails only when every sender was dropped without a send. Since the thread's panics are explicitly caught and resumed via join/resume_unwind, this panic means the control thread finished its closure without reaching the send — for example the closure exited early because the CEF message loop/browser context was torn down before the requested operation completed.

Source

Thrown at desktop/ui/src/context.rs:93

						host.close_browser(1);
					}
				});
				run_on_ui_thread(cef::quit_message_loop);
				match result {
					Ok(result) => {
						let _ = result_sender.send(result);
					}
					Err(panic) => std::panic::resume_unwind(panic),
				}
			})
			.expect("Failed to spawn the CEF control thread");
		cef::run_message_loop();
		drop(CONTEXT.take());
		cef::shutdown();
		if let Err(panic) = control_thread.join() {
			std::panic::resume_unwind(panic);
		}
		result_receiver.recv().expect("The CEF control thread ended without a result")
	}
}

pub(crate) fn execute_helper_process() -> std::process::ExitCode {
	let args = bootstrap(true);
	assert_eq!(args.as_cmd_line().unwrap().has_switch(Some(&"type".into())), 1, "Not a CEF helper process");
	let mut app = RenderProcessAppImpl::app();
	let code = execute_process(Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut());
	std::process::ExitCode::from(code as u8)
}

fn bootstrap(helper: bool) -> Args {
	#[cfg(target_os = "macos")]
	{
		let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), helper);
		assert!(loader.load());
		// LibraryLoader unloads the framework on drop
		std::mem::forget(loader);

View on GitHub (pinned to c507b35645)

Solutions

  1. Ensure the CEF context start/stop path is invoked exactly once per process (guard against double shutdown)
  2. Reproduce with CEF logging enabled (chrome_debug.log) to see how far the control thread got before teardown
  3. Patch the control-thread closure to send a sentinel/Err result on every early-exit path instead of returning silently
  4. If it happens after a CEF upgrade, review shutdown-ordering changes between the wrapper and cef::shutdown()

Example fix

// before
result_receiver.recv().expect("The CEF control thread ended without a result")

// after
match result_receiver.recv() {
	Ok(result) => result,
	Err(_) => {
		tracing::error!("CEF control thread ended without a result (context torn down early)");
		return;
	}
}
Defensive patterns

Strategy: fallback

Try / catch

match result_receiver.recv() {
	Ok(result) => result,
	Err(_) => {
		// Control thread exited without sending (context torn down early): treat as abnormal but orderly exit
		tracing::error!("CEF control thread ended without a result; assuming early shutdown");
		return;
	}
}

Prevention

When it happens

Trigger: Requesting a CEF operation (like browser close) while the context is already shutting down, so run_on_ui_thread callbacks never run and the closure returns without sending; calling the control path twice so the second run races shutdown; CEF shutdown ordering changes where cef::shutdown() terminates work still queued on the UI thread.

Common situations: Rapid window close/reopen or double-invocation of shutdown in the desktop UI; CEF version upgrades changing message-loop lifetime semantics; a shutdown race between the winit event loop exiting and the CEF control thread starting its work.

Related errors


AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16). Data as JSON: /api/errors/20641ef423c5e28f. Report an issue: GitHub.