Docs

Documentation

Error Handling Guide

Overview

This guide covers the error-handling building blocks that exist in the StratoSort Stack codebase today and when to reach for each one. Every function named here is a real export; if you find one that no longer matches the code, the code wins — please fix this page.

Layer Module
Response envelopes + codes src/shared/errorHandlingUtils.js, src/shared/errorCodes.js
IPC handler wrapping src/main/ipc/ipcWrappers.js
Timeouts, retries, cleanup src/shared/promiseUtils.js
Typed errors (main process) src/main/errors/AnalysisError.js, FileSystemError.js
Classification src/shared/errorClassifier.js, src/shared/llamaErrorClassifier.js
Process-level handler src/main/errors/ErrorHandler.js
Renderer display + boundaries src/renderer/utils/errorMapping.js, ErrorBoundary.jsx

1. Response envelopes (errorHandlingUtils.js)

src/shared/errorHandlingUtils.js exports ERROR_CODES, createErrorResponse, createSuccessResponse, withRetry, logFallback, getErrorMessage and attachErrorCode.

Error codes are defined in src/shared/errorCodes.js (ERROR_CODES) and re-exported here.

const {
  createErrorResponse,
  createSuccessResponse,
  attachErrorCode,
  ERROR_CODES
} = require('../../shared/errorHandlingUtils');

async function readSummary(filePath) {
  if (typeof filePath !== 'string' || !filePath) {
    return createErrorResponse(
      attachErrorCode(new Error('Invalid file path'), ERROR_CODES.INVALID_INPUT)
    );
  }
  try {
    const summary = await loadSummary(filePath);
    return createSuccessResponse({ summary }); // → { success: true, data: { summary } }
  } catch (error) {
    return createErrorResponse(error, { operation: 'readSummary' });
  }
}

2. IPC handlers (ipcWrappers.js)

Every IPC handler is registered through the IPC registry (src/main/core/ipcRegistry.js), which also applies ipcSenderValidator. Never call ipcMain.handle directly. The helpers in src/main/ipc/ipcWrappers.js:

Two behaviors to know:

  1. Validation failures return an envelope. An input that fails the schema returns a createErrorResponse failure (message 'Invalid input', details.errorType: 'ValidationError', details.validationErrors) without calling your handler.
  2. Thrown errors are logged and re-thrown. withErrorLogging (which createHandler always applies) logs and then re-throws, so ipcRenderer.invoke rejects. If the renderer should get a structured envelope instead, catch inside the handler and return createErrorResponse(...).
const { createHandler, safeHandle, z } = require('./ipcWrappers');

const handler = createHandler({
  logger,
  context: 'Analysis',
  schema: z.object({ filePath: z.string().min(1) }),
  handler: async (event, { filePath }) => performDocumentAnalysis(filePath)
});

safeHandle(ipcMain, IPC_CHANNELS.ANALYSIS.ANALYZE_DOCUMENT, handler);

See src/main/ipc/analysis.js for real examples.

Success shapes differ by helper. ipcWrappers.createSuccessResponse(data) spreads plain-object data into the response ({ success: true, ...data }) for backward compatibility, while the shared errorHandlingUtils.createSuccessResponse(data) nests it ({ success: true, data }). The simple helpers successResponse(data, warnings), errorResponse(error, extras) and canceledResponse() ({ success: false, canceled: true }) are used by settings-style handlers. Check which helper a channel uses before reading its result in the renderer. Failure envelopes from createErrorResponse always use the nested error: { code, message, details } shape.


3. Timeouts, retries and cleanup (promiseUtils.js)

const { retry, withTimeout, swallow } = require('../../shared/promiseUtils');

const result = await retry(() => withTimeout(fetchModelManifest(), 10000, 'FetchManifest'), {
  maxRetries: 3,
  initialDelay: 1000,
  shouldRetry: (error) => error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT'
});

await swallow(fs.promises.unlink(tempPath), logger, 'cleanup:temp-file');

errorHandlingUtils.js re-exports withRetry (same wrapper semantics) but not withTimeout or retry; import those from promiseUtils.js.


4. Typed errors (src/main/errors/)

Main-process code throws typed, operational errors (isOperational = true) that carry a code, structured metadata, getUserFriendlyMessage() and getActionableSteps().

AnalysisError.js — AnalysisError(code, metadata) (message is generated from code; pass { cause } in metadata to chain), plus ModelMissingError(modelName), DependencyMissingError(dependencyName), AiEngineConnectionError(details) and FileProcessingError(code, fileName, metadata).

FileSystemError.js — FileSystemError(code, metadata) with FILE_SYSTEM_ERROR_CODES and NODE_ERROR_CODE_MAP, plus isRecoverable(), shouldRetry(), getRetryDelay() and toJSON(). Convenience subclasses: FileNotFoundError, PermissionDeniedError, WriteFailedError, WatcherError, AtomicOperationError, IntegrityError. Convert raw Node errors with the static factories:

const { FileSystemError } = require('../errors/FileSystemError');

try {
  await fs.promises.rename(src, dest);
} catch (nodeError) {
  throw FileSystemError.forOperation('rename', nodeError, src);
  // or: FileSystemError.fromNodeError(nodeError, { path: src, operation: 'rename' })
}

ErrorHandler.js exports a singleton (not a class). simple-main.js initializes it; it listens for Electron render-process-gone / child-process-gone, writes to userData/logs, and escalates critical failures via handleCriticalError (uncaughtException / unhandledRejection are handled in src/main/core/lifecycle.js, not here). Call errorHandler.handleError(error, context) only for errors that should reach that central path; most code should throw or return an envelope instead.


5. Classification (errorClassifier.js)

src/shared/errorClassifier.js turns arbitrary errors (usually Node codes) into decisions:

Use isRetryable as the shouldRetry for filesystem retries rather than hand-rolling code lists. Errors from node-llama-cpp have their own classifier, src/shared/llamaErrorClassifier.js (classifyLlamaError plus looksLikeVramExhausted, looksLikeCorruptGguf, etc.).


6. Renderer: displaying errors and boundaries

Normalize IPC errors before rendering. The failure envelope's error is an object; placing it directly into JSX throws React error #31 ("Objects are not valid as a React child"). Convert it with toErrorMessage(response.error, fallback) from src/renderer/utils/errorMapping.js before putting it in state that feeds JSX or a notification. mapErrorToNotification({ error, errorType, errorCode, operationType }) builds richer toast content, and isIpcTimeoutError(error) detects preload invoke timeouts.

Error boundaries:

import ErrorBoundary from './components/ErrorBoundary';
import PhaseErrorBoundary from './components/PhaseErrorBoundary';

<PhaseErrorBoundary phaseName="Discover">
  <DiscoverPhase />
</PhaseErrorBoundary>;

<ErrorBoundary contextName="NavigationBar" fallback={() => null}>
  <NavigationBar />
</ErrorBoundary>;

Decision guide

Situation Use
New IPC handler createHandler + Zod schema, registered via safeHandle/registerHandlers
Handler must return a failure the UI can show catch and return createErrorResponse(error, context)
Filesystem failure in main FileSystemError.forOperation / fromNodeError
Analysis/model failure in main AnalysisError family
Deciding whether to retry isRetryable / classifyError, then retry or withRetry
Work that can hang withAbortableTimeout (cancellable) or withTimeout
Best-effort cleanup swallow
Catch block that returns a default logFallback
Showing an IPC error in the UI toErrorMessage / mapErrorToNotification
React subtree that may throw ErrorBoundary (contextName) or PhaseErrorBoundary

Best practices


Last Updated: 2026-10-27

Edit this page on GitHub