diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts
index 530cf95fb..29d7b0006 100644
--- a/apps/code/src/main/window.ts
+++ b/apps/code/src/main/window.ts
@@ -200,7 +200,7 @@ export function createWindow(): void {
...platformWindowConfig,
show: false,
webPreferences: {
- nodeIntegration: true,
+ nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, "preload.js"),
enableBlinkFeatures: "GetDisplayMedia",
diff --git a/apps/code/src/renderer/components/BootErrorBoundary.test.tsx b/apps/code/src/renderer/components/BootErrorBoundary.test.tsx
new file mode 100644
index 000000000..6ba04b933
--- /dev/null
+++ b/apps/code/src/renderer/components/BootErrorBoundary.test.tsx
@@ -0,0 +1,95 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+const { logError } = vi.hoisted(() => ({ logError: vi.fn() }));
+
+vi.mock("@posthog/ui/shell/logger", () => ({
+ logger: {
+ scope: () => ({
+ error: logError,
+ info: vi.fn(),
+ warn: vi.fn(),
+ debug: vi.fn(),
+ }),
+ },
+}));
+
+import {
+ BootErrorBoundary,
+ BootErrorScreen,
+} from "@components/BootErrorBoundary";
+
+function ThrowOnRender({ message }: { message: string }): never {
+ throw new Error(message);
+}
+
+describe("BootErrorScreen", () => {
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+ });
+
+ it("renders the alert, title and an Error message", () => {
+ render();
+
+ expect(screen.getByRole("alert")).toBeInTheDocument();
+ expect(
+ screen.getByText("PostHog Code failed to start"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("kaboom")).toBeInTheDocument();
+ });
+
+ it("stringifies a non-Error value", () => {
+ render();
+
+ expect(screen.getByText("plain string failure")).toBeInTheDocument();
+ });
+
+ it("reloads the window when Reload is clicked", () => {
+ const reload = vi.fn();
+ vi.stubGlobal("location", { reload });
+
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /reload/i }));
+
+ expect(reload).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe("BootErrorBoundary", () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it("renders children when no error is thrown", () => {
+ render(
+
+ healthy child
+ ,
+ );
+
+ expect(screen.getByText("healthy child")).toBeInTheDocument();
+ expect(screen.queryByRole("alert")).not.toBeInTheDocument();
+ });
+
+ it("catches a render error, shows the fallback and logs it", () => {
+ vi.spyOn(console, "error").mockImplementation(() => {});
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole("alert")).toBeInTheDocument();
+ expect(screen.getByText("render boom")).toBeInTheDocument();
+ expect(logError).toHaveBeenCalled();
+ });
+
+ it("derives error state from a thrown error", () => {
+ const error = new Error("derive");
+ expect(BootErrorBoundary.getDerivedStateFromError(error)).toEqual({
+ error,
+ });
+ });
+});
diff --git a/apps/code/src/renderer/components/BootErrorBoundary.tsx b/apps/code/src/renderer/components/BootErrorBoundary.tsx
new file mode 100644
index 000000000..28321676f
--- /dev/null
+++ b/apps/code/src/renderer/components/BootErrorBoundary.tsx
@@ -0,0 +1,100 @@
+import { logger } from "@posthog/ui/shell/logger";
+import React, { useEffect, useRef } from "react";
+
+const log = logger.scope("boot-error");
+
+// Inline styles intentionally: this screen must render even when the app's
+// theme, CSS or design-system providers failed to load during a boot failure.
+const screenStyle: React.CSSProperties = {
+ position: "fixed",
+ inset: 0,
+ outline: "none",
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ justifyContent: "center",
+ gap: 16,
+ padding: 24,
+ backgroundColor: "#0a0a0a",
+ color: "#fafafa",
+ fontFamily:
+ "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ textAlign: "center",
+};
+
+const titleStyle: React.CSSProperties = {
+ margin: 0,
+ fontSize: 18,
+ fontWeight: 600,
+};
+
+const messageStyle: React.CSSProperties = {
+ margin: 0,
+ maxWidth: 480,
+ fontSize: 13,
+ opacity: 0.8,
+};
+
+const buttonStyle: React.CSSProperties = {
+ padding: "6px 16px",
+ fontSize: 13,
+ fontWeight: 500,
+ color: "#0a0a0a",
+ backgroundColor: "#fafafa",
+ border: "none",
+ borderRadius: 6,
+ cursor: "pointer",
+};
+
+export function BootErrorScreen({ error }: { error: unknown }) {
+ const containerRef = useRef(null);
+ // Move focus to the alert on mount so screen readers announce it and keyboard
+ // users land on it, even when it is the very first thing rendered.
+ useEffect(() => {
+ containerRef.current?.focus();
+ }, []);
+ const message = error instanceof Error ? error.message : String(error);
+ return (
+
+
PostHog Code failed to start
+
{message}
+
+
+ );
+}
+
+interface BootErrorBoundaryProps {
+ children: React.ReactNode;
+}
+
+interface BootErrorBoundaryState {
+ error: Error | null;
+}
+
+export class BootErrorBoundary extends React.Component<
+ BootErrorBoundaryProps,
+ BootErrorBoundaryState
+> {
+ state: BootErrorBoundaryState = { error: null };
+
+ static getDerivedStateFromError(error: Error): BootErrorBoundaryState {
+ return { error };
+ }
+
+ componentDidCatch(error: Error, info: React.ErrorInfo): void {
+ log.error("Renderer crashed during render", error, info.componentStack);
+ }
+
+ render(): React.ReactNode {
+ if (this.state.error) {
+ return ;
+ }
+ return this.props.children;
+ }
+}
diff --git a/apps/code/src/renderer/main.tsx b/apps/code/src/renderer/main.tsx
index dfd059c3d..9d2092484 100644
--- a/apps/code/src/renderer/main.tsx
+++ b/apps/code/src/renderer/main.tsx
@@ -11,11 +11,16 @@ import "@renderer/di/container";
import "@renderer/platform-adapters/updates";
// Side effect: attaches window focus/visibility listeners so `focused` is accurate before inbox queries mount.
import "@posthog/ui/shell/rendererWindowFocusStore";
+import {
+ BootErrorBoundary,
+ BootErrorScreen,
+} from "@components/BootErrorBoundary";
import { Providers } from "@components/Providers";
import { preloadHighlighter } from "@pierre/diffs";
import { boot } from "@posthog/di/contribution";
import { ServiceProvider } from "@posthog/di/react";
import App from "@posthog/ui/shell/App";
+import { logger } from "@posthog/ui/shell/logger";
import { initializePostHog } from "@posthog/ui/shell/posthogAnalyticsImpl";
import { registerDesktopContributions } from "@renderer/desktop-contributions";
import { container } from "@renderer/di/container";
@@ -80,18 +85,34 @@ if (bootstrapSessionId) {
initializePostHog(bootstrapSessionId);
}
-registerDesktopContributions();
-void boot(container);
+const bootLog = logger.scope("renderer-boot");
const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Root element not found");
-ReactDOM.createRoot(rootElement).render(
-
-
-
-
-
-
- ,
-);
+const root = ReactDOM.createRoot(rootElement);
+
+try {
+ registerDesktopContributions();
+ boot(container).catch((error: unknown) => {
+ bootLog.error("Renderer boot sequence failed", error);
+ // Replaces the mounted tree without running effect cleanup; acceptable
+ // because a failed boot leaves the app unusable regardless.
+ root.render();
+ });
+
+ root.render(
+
+
+
+
+
+
+
+
+ ,
+ );
+} catch (error) {
+ bootLog.error("Renderer failed to start", error);
+ root.render();
+}