Error Handling & Message Normalization

How PayCal standardizes error reporting across all frontend modules to ensure users receive meaningful, secure, and consistent error feedback without exposing sensitive details.

Overview & Purpose

When users encounter errors, they deserve clear feedback explaining what happened and how to fix it. Raw backend error messages must be normalized to:

  • Remove noise: Strip redundant "Error:" prefixes and clean whitespace
  • Prevent leakage: Ensure sensitive implementation details never reach the user
  • Provide fallbacks: Display safe messages when errors are empty or malformed
  • Ensure consistency: Apply the same logic across all 11+ frontend modules
  • Improve debugging: Log full error details to Phantom Wing while showing safe summaries to users

The Problem: Generic vs. Meaningful Errors

Before standardization, PayCal modules used ad-hoc error handling:

// ❌ BAD: Exposes raw error, duplicates logic
PC.showToast(error?.message || 'Import failed.');
PW.error(`Import failed: ${error.message}`);

Problems with this approach:

  • Users see confusing raw messages like "ECONNREFUSED: Connection refused"
  • Each module implements its own fallback logic independently
  • No consistent whitespace trimming or prefix stripping
  • Empty error messages can display as "undefined" in the UI

The Solution: Standardized Error Resolver

All PayCal frontend modules now use a unified resolver function that normalizes error messages:

// ✅ GOOD: Normalized, consistent, safe
const resolveThrownMessage = (error, fallbackMessage) => {
  const raw = typeof error?.message === 'string' 
    ? error.message 
    : String(error || '');
  const normalized = raw.replace(/^Error:\s*/i, '').trim();
  return normalized !== '' ? normalized : fallbackMessage;
};

Usage:

// In catch blocks across modules
try {
  await updateProfile(data);
} catch (error) {
  const message = resolveThrownMessage(error, 'Unable to update profile.');
  PC.showToast(message, 'error');  // User sees meaningful feedback
  PW.error(message);                // Logged for debugging
}

Implementation Scope

As of April 2026, this standardized error-handling pattern has been applied to 11 frontend modules with approximately 40+ normalized catch blocks:

Authentication & Settings (7 modules)

  • html/js/auth-recovery/index.php (4 catches)
  • html/js/signin/index.php (2 catches)
  • html/js/signin/verification-reminder.js (2 catches)
  • html/js/signin/verification-status-banner.js (1 catch)
  • html/js/settings/index.php (8+ catches)

Core & Data Modules (4 modules)

  • html/js/core/network.js (3 catches)
  • html/js/core/index.php (5 catches)
  • html/js/core/billing.js (5 catches)
  • html/js/earnings/index.php (4 catches)

High-value modules (10+ catch points):

  • html/js/organizations/index.php - Org management, access requests, audit trails (19+ catches)
  • html/js/sites/index.php - Site CRUD, earnings, orphan work recovery (10+ catches)
  • html/js/calendar/calendar.js - Day-entry operations, copy/paste/delete (2 catches)

Error Categories & Handling Patterns

The resolver is applied consistently across several error categories:

1. Network Request Failures

// Network module: HTTP errors, timeouts, connection issues
async function deleteResource(ep, id) {
  try {
    // ...fetch logic...
  } catch (error) {
    const resolved = resolveThrownMessage(error, 'Network error');
    const msg = `[deleteResource] ${resolved}`;
    PW.error(msg);
    throw new Error(msg);
  }
}

2. API Response Handling

// Billing/Settings: Server returned error message in payload
try {
  const response = await fetch('/api/v1/billing/subscription');
  const payload = await response.json();
  if (!response.ok) {
    throw new Error(payload?.message || 'Unable to load billing status.');
  }
} catch (error) {
  const resolved = resolveThrownMessage(error, 'Unable to load billing status.');
  setScreenReaderStatus(resolved);
}

3. UI Operation Failures

// Calendar/Organizations: User-initiated actions (paste, delete, update)
button.addEventListener('click', async () => {
  try {
    await performAction();
    PC.showToast('Success!', 'save');
  } catch (error) {
    const message = resolveThrownMessage(error, 'Action failed. Try again.');
    PC.showToast(message, 'error');
  }
});

4. Async Initialization

// Core modules: Startup or dependent initialization failures
try {
  NavigationToggle.init();
} catch (err) {
  const resolved = resolveThrownMessage(err, 'Navigation init failed');
  PW.warn(resolved);  // Logged but doesn't block page
}

Security Considerations

Error message normalization protects user privacy and system integrity:

  • No Database Details: Backend errors like "UNIQUE constraint failed on email" are intercepted at the API boundary and replaced with user-friendly messages
  • No File Paths: System errors exposing file paths or process details are stripped
  • No Auth Leakage: Responses to authentication failures never reveal whether an account exists (timing-safe generic messages only)
  • No CORS/Network Details: Transport-layer errors are normalized to generic "Connection error" messages
  • Secure Fallbacks: All catchers have explicit fallback messages; never displays "undefined" or "null"

User Experience Benefits

This standardization gives users clearer, safer, and more consistent feedback while making support and debugging easier for the team.

  • Messages are understandable without exposing implementation detail
  • Fallback text remains safe when the backend error is missing or malformed
  • Logging stays useful for debugging without leaking sensitive values to the UI

Debugging & Support Workflow

Support staff can rely on a consistent resolver path, then inspect Phantom Wing logs for the full technical detail when needed.

  • User sees normalized message
  • Phantom Wing records the full error context privately
  • Support can correlate issue class without exposing the raw backend response

Testing & Quality Assurance

The normalization logic is covered by focused module tests and by integration checks across the main frontend flows.

Maintenance & Future Extensions

As PayCal adds more modules, the same resolver pattern should be reused instead of duplicating custom error formatting.

  • New modules should call the shared resolver directly
  • Module-specific fallback messages should stay concise and user-facing
  • Any changes to the resolver should be regression-tested across representative flows

That keeps behavior predictable as the codebase grows and avoids a second generation of module-specific error handling.

Summary: The PayCal Error-Handling Standard

PayCal standardized frontend error handling so that users get safe, consistent messages while developers still get the information they need for debugging.

  • Normalize error messages before display
  • Hide implementation details from users
  • Use explicit fallback text for empty or malformed errors
  • Log technical detail privately for support and debugging