function isNonFatalNetworkError(err) { if (!err) return false; // Any error with an ssh2 `level` property is a connection/auth-level error, // never a reason to kill the entire multi-session app. if (err.level) return true; const candidates = [err, err.cause].filter(Boolean); for (const candidate of candidates) { const code = candidate.code; // Common TCP/DNS/routing errors that can surface from Node.js sockets // without an ssh2 `level` (e.g. proxy sockets, raw net.connect calls). switch (code) { case "ECONNRESET": case "ECONNREFUSED": case "ECONNABORTED": case "ETIMEDOUT": case "ENOTFOUND": case "EHOSTUNREACH": case "EHOSTDOWN": case "ENETUNREACH": case "ENETDOWN": case "EADDRNOTAVAIL": case "EPROTO": case "EPERM": return true; default: break; } // Chromium/Electron networking often rejects with a message like // "net::ERR_NETWORK_CHANGED" but without a useful `code` property. // These are transport failures for background fetch/update/sync work, // not reasons to kill the whole app. const message = String(candidate.message || ""); if (/net::ERR_(?:NETWORK_[A-Z_]+|INTERNET_DISCONNECTED|NAME_NOT_RESOLVED|CONNECTION_[A-Z_]+|ADDRESS_[A-Z_]+|SSL_[A-Z_]+|CERT_[A-Z_]+|PROXY_[A-Z_]+|TUNNEL_[A-Z_]+|SOCKS_[A-Z_]+)/.test(message)) { return true; } } return false; } function isBenignStreamError(err) { const code = err?.code; return code === "EPIPE" || code === "ERR_STREAM_DESTROYED"; } function classifyProcessError(err, options = {}) { const runtimeStarted = options.runtimeStarted === true; if (isBenignStreamError(err)) { return { action: "ignore", reason: "benign stream teardown", }; } if (isNonFatalNetworkError(err)) { return { action: "suppress", reason: "non-fatal network error", }; } if (runtimeStarted) { return { action: "suppress", reason: "runtime error after startup", }; } return { action: "fatal", reason: "startup error before app became usable", }; } function createProcessErrorController(options = {}) { const captureError = typeof options.captureError === "function" ? options.captureError : () => {}; const onFatalError = typeof options.onFatalError === "function" ? options.onFatalError : (err) => { throw err; }; const logError = typeof options.logError === "function" ? options.logError : (...args) => console.error(...args); const logWarn = typeof options.logWarn === "function" ? options.logWarn : (...args) => console.warn(...args); let hasShownMainWindow = false; let pendingMainWindowStartupCount = 0; const isRuntimeProtectionActive = () => ( hasShownMainWindow && pendingMainWindowStartupCount === 0 ); const beginMainWindowStartup = () => { pendingMainWindowStartupCount += 1; }; const completeMainWindowStartup = ({ windowShown = false } = {}) => { if (pendingMainWindowStartupCount > 0) { pendingMainWindowStartupCount -= 1; } if (windowShown) { hasShownMainWindow = true; } }; const handleUncaughtException = (err) => { const decision = classifyProcessError(err, { runtimeStarted: isRuntimeProtectionActive(), origin: "uncaughtException", }); if (decision.action === "ignore") { logWarn("Ignored process error:", decision.reason, err?.code || err?.message || err); return; } if (decision.action === "suppress") { if (!err?.__fromUnhandledRejection) { captureError("uncaughtException", err); } logError(`Suppressed uncaught exception (${decision.reason}):`, err); return; } if (!err?.__fromUnhandledRejection) { captureError("uncaughtException", err); } onFatalError(err, { origin: "uncaughtException", decision, reason: err, }); }; const handleUnhandledRejection = (reason) => { const decision = classifyProcessError(reason, { runtimeStarted: isRuntimeProtectionActive(), origin: "unhandledRejection", }); if (decision.action === "ignore") { return; } if (decision.action === "suppress") { captureError("unhandledRejection", reason); logError(`Suppressed unhandled rejection (${decision.reason}):`, reason); return; } captureError("unhandledRejection", reason); const err = reason instanceof Error ? reason : new Error(String(reason)); err.__fromUnhandledRejection = true; onFatalError(err, { origin: "unhandledRejection", decision, reason, }); }; return { beginMainWindowStartup, completeMainWindowStartup, handleUncaughtException, handleUnhandledRejection, isRuntimeProtectionActive, }; } function installProcessErrorHandlers(processObject, controller) { if (!processObject?.on || !processObject?.removeListener) { throw new Error("A process-like EventEmitter is required"); } if (!controller?.handleUncaughtException || !controller?.handleUnhandledRejection) { throw new Error("A process error controller is required"); } processObject.on("uncaughtException", controller.handleUncaughtException); processObject.on("unhandledRejection", controller.handleUnhandledRejection); return () => { processObject.removeListener("uncaughtException", controller.handleUncaughtException); processObject.removeListener("unhandledRejection", controller.handleUnhandledRejection); }; } module.exports = { classifyProcessError, createProcessErrorController, installProcessErrorHandlers, isBenignStreamError, isNonFatalNetworkError, };