refactor: centralize unhandled rejection setup

This commit is contained in:
Peter Steinberger
2026-01-07 20:59:49 +00:00
parent fd3babc626
commit 9bd439892f
7 changed files with 43 additions and 45 deletions

View File

@@ -6,7 +6,7 @@ import { normalizeEnv } from "../infra/env.js";
import { isMainModule } from "../infra/is-main.js"; import { isMainModule } from "../infra/is-main.js";
import { ensureClawdbotCliOnPath } from "../infra/path-env.js"; import { ensureClawdbotCliOnPath } from "../infra/path-env.js";
import { assertSupportedRuntime } from "../infra/runtime-guard.js"; import { assertSupportedRuntime } from "../infra/runtime-guard.js";
import { isUnhandledRejectionHandled } from "../infra/unhandled-rejections.js"; import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
import { enableConsoleCapture } from "../logging.js"; import { enableConsoleCapture } from "../logging.js";
export async function runCli(argv: string[] = process.argv) { export async function runCli(argv: string[] = process.argv) {
@@ -25,14 +25,7 @@ export async function runCli(argv: string[] = process.argv) {
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions. // Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
// These log the error and exit gracefully instead of crashing without trace. // These log the error and exit gracefully instead of crashing without trace.
process.on("unhandledRejection", (reason, _promise) => { installUnhandledRejectionHandler();
if (isUnhandledRejectionHandled(reason)) return;
console.error(
"[clawdbot] Unhandled promise rejection:",
reason instanceof Error ? (reason.stack ?? reason.message) : reason,
);
process.exit(1);
});
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
console.error( console.error(

View File

@@ -27,7 +27,7 @@ import {
PortInUseError, PortInUseError,
} from "./infra/ports.js"; } from "./infra/ports.js";
import { assertSupportedRuntime } from "./infra/runtime-guard.js"; import { assertSupportedRuntime } from "./infra/runtime-guard.js";
import { isUnhandledRejectionHandled } from "./infra/unhandled-rejections.js"; import { installUnhandledRejectionHandler } from "./infra/unhandled-rejections.js";
import { enableConsoleCapture } from "./logging.js"; import { enableConsoleCapture } from "./logging.js";
import { runCommandWithTimeout, runExec } from "./process/exec.js"; import { runCommandWithTimeout, runExec } from "./process/exec.js";
import { monitorWebProvider } from "./provider-web.js"; import { monitorWebProvider } from "./provider-web.js";
@@ -79,14 +79,7 @@ const isMain = isMainModule({
if (isMain) { if (isMain) {
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions. // Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
// These log the error and exit gracefully instead of crashing without trace. // These log the error and exit gracefully instead of crashing without trace.
process.on("unhandledRejection", (reason, _promise) => { installUnhandledRejectionHandler();
if (isUnhandledRejectionHandled(reason)) return;
console.error(
"[clawdbot] Unhandled promise rejection:",
reason instanceof Error ? (reason.stack ?? reason.message) : reason,
);
process.exit(1);
});
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
console.error( console.error(

14
src/infra/bonjour-ciao.ts Normal file
View File

@@ -0,0 +1,14 @@
import { logDebug } from "../logger.js";
import { formatBonjourError } from "./bonjour-errors.js";
export function ignoreCiaoCancellationRejection(reason: unknown): boolean {
const message = formatBonjourError(reason).toUpperCase();
if (!message.includes("CIAO ANNOUNCEMENT CANCELLED")) {
return false;
}
logDebug(
`bonjour: ignoring unhandled ciao rejection: ${formatBonjourError(reason)}`,
);
return true;
}

View File

@@ -0,0 +1,7 @@
export function formatBonjourError(err: unknown): string {
if (err instanceof Error) {
const msg = err.message || String(err);
return err.name && err.name !== "Error" ? `${err.name}: ${msg}` : msg;
}
return String(err);
}

View File

@@ -2,6 +2,8 @@ import os from "node:os";
import { logDebug, logWarn } from "../logger.js"; import { logDebug, logWarn } from "../logger.js";
import { getLogger } from "../logging.js"; import { getLogger } from "../logging.js";
import { ignoreCiaoCancellationRejection } from "./bonjour-ciao.js";
import { formatBonjourError } from "./bonjour-errors.js";
import { registerUnhandledRejectionHandler } from "./unhandled-rejections.js"; import { registerUnhandledRejectionHandler } from "./unhandled-rejections.js";
export type GatewayBonjourAdvertiser = { export type GatewayBonjourAdvertiser = {
@@ -45,14 +47,6 @@ type BonjourService = {
serviceState: string; serviceState: string;
}; };
function formatBonjourError(err: unknown): string {
if (err instanceof Error) {
const msg = err.message || String(err);
return err.name && err.name !== "Error" ? `${err.name}: ${msg}` : msg;
}
return String(err);
}
function serviceSummary(label: string, svc: BonjourService): string { function serviceSummary(label: string, svc: BonjourService): string {
let fqdn = "unknown"; let fqdn = "unknown";
let hostname = "unknown"; let hostname = "unknown";
@@ -147,16 +141,7 @@ export async function startGatewayBonjourAdvertiser(
let ciaoCancellationRejectionHandler: (() => void) | undefined; let ciaoCancellationRejectionHandler: (() => void) | undefined;
if (services.length > 0) { if (services.length > 0) {
ciaoCancellationRejectionHandler = registerUnhandledRejectionHandler( ciaoCancellationRejectionHandler = registerUnhandledRejectionHandler(
(reason) => { ignoreCiaoCancellationRejection,
const message = formatBonjourError(reason).toUpperCase();
if (!message.includes("CIAO ANNOUNCEMENT CANCELLED")) {
return false;
}
logDebug(
`bonjour: ignoring unhandled ciao rejection: ${formatBonjourError(reason)}`,
);
return true;
},
); );
} }

View File

@@ -1,3 +1,5 @@
import process from "node:process";
type UnhandledRejectionHandler = (reason: unknown) => boolean; type UnhandledRejectionHandler = (reason: unknown) => boolean;
const handlers = new Set<UnhandledRejectionHandler>(); const handlers = new Set<UnhandledRejectionHandler>();
@@ -24,3 +26,14 @@ export function isUnhandledRejectionHandled(reason: unknown): boolean {
} }
return false; return false;
} }
export function installUnhandledRejectionHandler(): void {
process.on("unhandledRejection", (reason, _promise) => {
if (isUnhandledRejectionHandled(reason)) return;
console.error(
"[clawdbot] Unhandled promise rejection:",
reason instanceof Error ? (reason.stack ?? reason.message) : reason,
);
process.exit(1);
});
}

View File

@@ -59,21 +59,14 @@ async function main() {
const { assertSupportedRuntime } = await import("../infra/runtime-guard.js"); const { assertSupportedRuntime } = await import("../infra/runtime-guard.js");
assertSupportedRuntime(); assertSupportedRuntime();
const { isUnhandledRejectionHandled } = await import( const { installUnhandledRejectionHandler } = await import(
"../infra/unhandled-rejections.js" "../infra/unhandled-rejections.js"
); );
const { buildProgram } = await import("../cli/program.js"); const { buildProgram } = await import("../cli/program.js");
const program = buildProgram(); const program = buildProgram();
process.on("unhandledRejection", (reason, _promise) => { installUnhandledRejectionHandler();
if (isUnhandledRejectionHandled(reason)) return;
console.error(
"[clawdbot] Unhandled promise rejection:",
reason instanceof Error ? (reason.stack ?? reason.message) : reason,
);
process.exit(1);
});
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
console.error( console.error(