From 384350f73174af723e993b28578cc1986a0701de Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 21 Jan 2026 14:07:04 +0100 Subject: [PATCH] feat: add type-safe error handling utilities - Add isError() type guard to check if value is Error instance - Add getErrorMessage() utility for safe error message extraction in catch blocks (handles TypeScript 4.4+ unknown error type) - Replace all (err as Error).message patterns with getErrorMessage(err) across server.ts, cli.ts, ralph-loop.ts, and screen-manager.ts - Follows TypeScript best practice of treating caught errors as unknown This improves code safety by properly handling the case where caught values may not be Error instances (e.g., thrown strings or objects). Co-Authored-By: Claude Opus 4.5 --- src/cli.ts | 9 +++++---- src/ralph-loop.ts | 6 +++--- src/screen-manager.ts | 4 ++-- src/types.ts | 40 ++++++++++++++++++++++++++++++++++++ src/web/server.ts | 47 ++++++++++++++++++++++--------------------- 5 files changed, 74 insertions(+), 32 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 7fc4c203..e27fec95 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,6 +13,7 @@ import { getSessionManager } from './session-manager.js'; import { getTaskQueue } from './task-queue.js'; import { getRalphLoop } from './ralph-loop.js'; import { getStore } from './state-store.js'; +import { getErrorMessage } from './types.js'; const program = new Command(); @@ -40,7 +41,7 @@ sessionCmd console.log(` Working directory: ${session.workingDir}`); console.log(` PID: ${session.pid}`); } catch (err) { - console.error(chalk.red(`✗ Failed to start session: ${(err as Error).message}`)); + console.error(chalk.red(`✗ Failed to start session: ${getErrorMessage(err)}`)); process.exit(1); } }); @@ -54,7 +55,7 @@ sessionCmd await manager.stopSession(id); console.log(chalk.green(`✓ Session stopped: ${id}`)); } catch (err) { - console.error(chalk.red(`✗ Failed to stop session: ${(err as Error).message}`)); + console.error(chalk.red(`✗ Failed to stop session: ${getErrorMessage(err)}`)); process.exit(1); } }); @@ -474,7 +475,7 @@ program console.log(chalk.green(`\n✓ Web interface running at http://localhost:${port}`)); console.log(chalk.gray(' Press Ctrl+C to stop\n')); } catch (err) { - console.error(chalk.red(`✗ Failed to start web server: ${(err as Error).message}`)); + console.error(chalk.red(`✗ Failed to start web server: ${getErrorMessage(err)}`)); process.exit(1); } }); @@ -496,7 +497,7 @@ program port, }); } catch (err) { - console.error(chalk.red(`✗ Failed to start TUI: ${(err as Error).message}`)); + console.error(chalk.red(`✗ Failed to start TUI: ${getErrorMessage(err)}`)); process.exit(1); } }); diff --git a/src/ralph-loop.ts b/src/ralph-loop.ts index 695b483a..a996886f 100644 --- a/src/ralph-loop.ts +++ b/src/ralph-loop.ts @@ -19,7 +19,7 @@ import { getTaskQueue, TaskQueue } from './task-queue.js'; import { getStore, StateStore } from './state-store.js'; import { Session } from './session.js'; import { Task } from './task.js'; -import { RalphLoopStatus } from './types.js'; +import { RalphLoopStatus, getErrorMessage } from './types.js'; /** * Events emitted by RalphLoop @@ -304,10 +304,10 @@ export class RalphLoop extends EventEmitter { this.emit('taskAssigned', task.id, session.id); } catch (err) { - task.fail((err as Error).message); + task.fail(getErrorMessage(err)); session.clearTask(); this.taskQueue.updateTask(task); - this.emit('taskFailed', task.id, (err as Error).message); + this.emit('taskFailed', task.id, getErrorMessage(err)); } } diff --git a/src/screen-manager.ts b/src/screen-manager.ts index 98df15ea..c6b443a0 100644 --- a/src/screen-manager.ts +++ b/src/screen-manager.ts @@ -19,7 +19,7 @@ import { spawn, execSync } from 'node:child_process'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { homedir } from 'node:os'; -import { ScreenSession, ProcessStats, ScreenSessionWithStats } from './types.js'; +import { ScreenSession, ProcessStats, ScreenSessionWithStats, getErrorMessage } from './types.js'; /** Path to persisted screen session metadata */ const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json'); @@ -213,7 +213,7 @@ export class ScreenManager extends EventEmitter { return screen; } catch (err) { - throw new Error(`Failed to create screen: ${(err as Error).message}`); + throw new Error(`Failed to create screen: ${getErrorMessage(err)}`); } } diff --git a/src/types.ts b/src/types.ts index 1e6299d6..ddcde499 100644 --- a/src/types.ts +++ b/src/types.ts @@ -615,3 +615,43 @@ export function createInitialState(): AppState { config: { ...DEFAULT_CONFIG }, }; } + +// ========== Error Handling Utilities ========== + +/** + * Type guard to check if a value is an Error instance + * @param value The value to check + * @returns True if the value is an Error instance + */ +export function isError(value: unknown): value is Error { + return value instanceof Error; +} + +/** + * Safely extracts an error message from an unknown caught value. + * Handles the TypeScript 4.4+ unknown error type in catch blocks. + * + * @param error The caught error (type unknown in strict mode) + * @returns A string error message + * + * @example + * ```typescript + * try { + * await riskyOperation(); + * } catch (err) { + * console.error('Failed:', getErrorMessage(err)); + * } + * ``` + */ +export function getErrorMessage(error: unknown): string { + if (isError(error)) { + return error.message; + } + if (typeof error === 'string') { + return error; + } + if (error && typeof error === 'object' && 'message' in error) { + return String((error as { message: unknown }).message); + } + return 'An unknown error occurred'; +} diff --git a/src/web/server.ts b/src/web/server.ts index 30d1791f..58eec2ff 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -23,19 +23,20 @@ import { ScreenManager } from '../screen-manager.js'; import { getStore } from '../state-store.js'; import { generateClaudeMd } from '../templates/claude-md.js'; import { v4 as uuidv4 } from 'uuid'; -import type { - CreateSessionRequest, - RunPromptRequest, - SessionInputRequest, - ResizeRequest, - CreateCaseRequest, - QuickStartRequest, - CreateScheduledRunRequest, - QuickRunRequest, - ApiResponse, - SessionResponse, - QuickStartResponse, - CaseInfo, +import { + getErrorMessage, + type CreateSessionRequest, + type RunPromptRequest, + type SessionInputRequest, + type ResizeRequest, + type CreateCaseRequest, + type QuickStartRequest, + type CreateScheduledRunRequest, + type QuickRunRequest, + type ApiResponse, + type SessionResponse, + type QuickStartResponse, + type CaseInfo, } from '../types.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -376,7 +377,7 @@ export class WebServer extends EventEmitter { this.broadcast('session:updated', { session: session.toDetailedState() }); return { success: true, message: 'Interactive session started' }; } catch (err) { - return { error: (err as Error).message }; + return { error: getErrorMessage(err) }; } }); @@ -399,7 +400,7 @@ export class WebServer extends EventEmitter { this.broadcast('session:updated', { session: session.toDetailedState() }); return { success: true, message: 'Shell session started' }; } catch (err) { - return { error: (err as Error).message }; + return { error: getErrorMessage(err) }; } }); @@ -580,7 +581,7 @@ export class WebServer extends EventEmitter { respawnStatus: controller.getStatus(), }; } catch (err) { - return { error: (err as Error).message }; + return { error: getErrorMessage(err) }; } }); @@ -694,7 +695,7 @@ export class WebServer extends EventEmitter { } catch (err) { // Clean up session on error too await this.cleanupSession(session.id); - return { success: false, sessionId: session.id, error: (err as Error).message }; + return { success: false, sessionId: session.id, error: getErrorMessage(err) }; } }); @@ -809,7 +810,7 @@ export class WebServer extends EventEmitter { return { success: true, case: { name, path: casePath } }; } catch (err) { - return { success: false, error: (err as Error).message }; + return { success: false, error: getErrorMessage(err) }; } }); @@ -868,7 +869,7 @@ export class WebServer extends EventEmitter { this.broadcast('case:linked', { name, path: expandedPath }); return { success: true, case: { name, path: expandedPath } }; } catch (err) { - return { success: false, error: (err as Error).message }; + return { success: false, error: getErrorMessage(err) }; } }); @@ -944,7 +945,7 @@ export class WebServer extends EventEmitter { this.broadcast('case:created', { name: caseName, path: casePath }); } catch (err) { - return { success: false, error: `Failed to create case: ${(err as Error).message}` }; + return { success: false, error: `Failed to create case: ${getErrorMessage(err)}` }; } } @@ -996,7 +997,7 @@ export class WebServer extends EventEmitter { } catch (err) { // Clean up session on error to prevent orphaned resources await this.cleanupSession(session.id); - return { success: false, error: (err as Error).message }; + return { success: false, error: getErrorMessage(err) }; } }); @@ -1026,7 +1027,7 @@ export class WebServer extends EventEmitter { writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); return { success: true }; } catch (err) { - return { error: (err as Error).message }; + return { error: getErrorMessage(err) }; } }); @@ -1415,7 +1416,7 @@ export class WebServer extends EventEmitter { // Small pause between iterations await new Promise(r => setTimeout(r, 2000)); } catch (err) { - addLog(`Error: ${(err as Error).message}`); + addLog(`Error: ${getErrorMessage(err)}`); this.broadcast('scheduled:updated', run); // Clean up the session on error too