Claude Code Plugins

Community-maintained marketplace

Feedback

fvtt-error-handling

@ImproperSubset/hh-agentics
0
0

This skill should be used when adding error handling to catch blocks, standardizing error handling across a codebase, or ensuring proper UX with user messages vs technical logs. Covers NotificationOptions, Hooks.onError, and preventing console noise.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name fvtt-error-handling
description This skill should be used when adding error handling to catch blocks, standardizing error handling across a codebase, or ensuring proper UX with user messages vs technical logs. Covers NotificationOptions, Hooks.onError, and preventing console noise.

Foundry VTT Error Handling Patterns

Domain: Foundry VTT V12/V13 Error Handling Status: Production-Ready (Verified against V13 API) Last Updated: 2025-12-31


Overview

This skill provides production-ready error handling patterns for Foundry VTT modules, using the documented V13 APIs: NotificationOptions and Hooks.onError.

When to Use This Skill

  • Adding error handling to catch blocks in Foundry modules
  • Standardizing error handling across a codebase
  • Ensuring proper UX (user messages vs technical logs)
  • Preventing console noise and double-logging
  • Enabling ecosystem compatibility (other modules can listen to your errors)

Core Principle

Always separate error funnel from user notification:

  • Error funnel (Hooks.onError): Logs stack traces, triggers ecosystem hooks, provides structured data
  • User notification (ui.notifications.*): Clean, sanitized messages with full UX control

Why separation matters:

  1. Hooks.onError mutates error.message when msg is provided (see Foundry GitHub #6669)
  2. clean: true only works in NotificationOptions, not Hooks.onError
  3. User sees only your controlled message, not technical details
  4. Explicit control over console logging (console: false prevents double-logging)

Quick Reference: Four Error Patterns

Pattern Error Funnel User Notification Use Case
User-facing Hooks.onError (notify: null) ui.notifications.error Unexpected failures
Expected validation (none) ui.notifications.warn User input errors
Developer-only Hooks.onError (notify: null) (none) Diagnostic logging
High-frequency Throttled Hooks.onError Throttled ui.notifications.warn Render loops, hooks

Pattern 1: User-Facing Failures

Use for: Unexpected errors, operations that failed, critical failures

} catch (err) {
  // Preserve original error as cause when wrapping non-Errors
  const error = err instanceof Error ? err : new Error(String(err), { cause: err });

  // Error funnel: stack traces + ecosystem hooks (no UI)
  Hooks.onError(`YourModule.${contextDescription}`, error, {
    msg: "[YourModule]",
    log: "error",
    notify: null,  // No UI from hook
    data: { contextDescription, userFacingDescription }  // Structured context
  });

  // Fully controlled user message (sanitized, no console - already logged)
  ui.notifications.error(`[YourModule] ${userFacingDescription}`, {
    clean: true,
    console: false  // Hooks.onError already logged
  });
}

Key points:

  • User sees only userFacingDescription (no technical details leaked)
  • Stack trace logged via Hooks.onError (full Error object)
  • Ecosystem visibility (other modules can listen to Hooks.on("error", ...))
  • Structured data for debugging and hook subscribers
  • Explicit console: false prevents double-logging

Pattern 2: Expected Validation / Recoverable Issues

Use for: User input errors, missing data, expected validation failures

} catch (err) {
  const message = `[YourModule] ${userFacingDescription}`;
  ui.notifications.warn(message, {
    clean: true,
    console: false  // Expected failures - no console noise
  });
}

Key points:

  • Uses warn severity (not error) for expected cases
  • Simpler than Hooks.onError (no error funnel needed for routine validation)
  • console: false avoids noise for common user-driven failures
  • Optional: Gate console logging behind debug flag:
    console: game.settings.get("your-module", "enableProfiling")
    

Pattern 3: Developer-Only Errors

Use for: Diagnostic logging, internal errors that don't need user notification

} catch (err) {
  const error = err instanceof Error ? err : new Error(String(err), { cause: err });
  Hooks.onError(`YourModule.${contextDescription}`, error, {
    msg: "[YourModule]",
    log: "error",
    notify: null,  // Log only, no UI spam
    data: { contextDescription }  // Structured context for debugging
  });
}

Key points:

  • Uses error funnel (consistency) but no notification
  • Stack traces logged for developers
  • No UI spam for internal diagnostics
  • Alternative: Use console.error(...) for truly isolated cases (but lose ecosystem hooks)

Pattern 4: High-Frequency Errors

Use for: Render loops, hooks, event handlers that might error repeatedly

// Module-scoped throttle (outside class/function)
const errorThrottles = new Map();

// In catch block:
} catch (err) {
  const throttleKey = contextDescription;
  const lastError = errorThrottles.get(throttleKey) || 0;

  // Throttle: 5 second window per context
  if (Date.now() - lastError > 5000) {
    const error = err instanceof Error ? err : new Error(String(err), { cause: err });

    // Error funnel (no UI)
    Hooks.onError(`YourModule.${contextDescription}`, error, {
      msg: "[YourModule]",
      log: "warn",
      notify: null,  // No UI from hook
      data: { contextDescription, userFacingDescription }
    });

    // Separate controlled notification (console: false prevents double-logging)
    ui.notifications.warn(`[YourModule] ${userFacingDescription}`, {
      clean: true,
      console: false  // Already logged via Hooks.onError
    });

    errorThrottles.set(throttleKey, Date.now());
  }
}

Key points:

  • Throttles to prevent notification queue flood
  • Module-scoped Map prevents spam across multiple instances
  • Uses warn severity for non-critical repeated errors
  • Separates error funnel (logs only) from user notification
  • Explicit console: false prevents double-logging

Decision Tree: Classifying Errors

Ask these questions:

  1. Is this unexpected? (system failure, unhandled case, critical error) → Pattern 1: User-facing failures

  2. Is this expected validation? (user input error, missing required data) → Pattern 2: Expected validation

  3. Is this diagnostic-only? (internal state, no user impact) → Pattern 3: Developer-only

  4. Could this fire repeatedly? (render loop, hook, event handler) → Pattern 4: High-frequency throttled


Foundry-Specific Gotchas

1. Hooks.onError Mutates Error Objects

Critical: Hooks.onError modifies error.message when msg is provided:

// Internal Foundry implementation (from GitHub #6669):
if (msg) err.message = `${msg}: ${err.message}`;

This is why we separate funnel from notification:

  • Error funnel gets the Error object (stack traces preserved)
  • User notification uses clean string (no mutation affects UX)

Always normalize to Error objects:

const error = err instanceof Error ? err : new Error(String(err), { cause: err });

2. console Default Behavior Not Documented

Issue: Foundry V13 docs don't explicitly state the default value for NotificationOptions.console.

Evidence: API examples show { console: false } to suppress logging, implying default is true.

Best practice: Be explicit to prevent double-logging and future-proof against default changes:

ui.notifications.error(message, {
  clean: true,
  console: false  // Explicit is better than implicit
});

3. notify String Values Are Undocumented

Issue: Hooks.onError accepts notify: null | string, but valid string values aren't enumerated in V13 API docs.

Your assumption: notify accepts "error", "warn", "info" (like ui.notifications.* methods)

Reality: Likely correct but NOT explicitly documented.

Best practice: Use notify: null + separate ui.notifications.* for full control:

// Avoid relying on undocumented notify strings
Hooks.onError(..., { notify: null });  // Error funnel only
ui.notifications.error(...);           // Separate notification

4. clean: true Only Works in NotificationOptions

Issue: { clean: true } sanitizes untrusted input, but ONLY in ui.notifications.*.

Does NOT work in Hooks.onError: The msg parameter is NOT sanitized by Hooks.onError.

Best practice:

  • Keep msg in Hooks.onError generic (module prefix only): msg: "[YourModule]"
  • Put user/document data in separate ui.notifications.* call with { clean: true }
// ❌ Bad - untrusted data in Hooks.onError msg
Hooks.onError(..., { msg: `[Module] ${userInput}` });  // Not sanitized!

// ✅ Good - untrusted data in ui.notifications with clean: true
Hooks.onError(..., { msg: "[Module]", notify: null });
ui.notifications.error(`[Module] ${userInput}`, { clean: true });

5. { cause: err } Has Excellent Support

Good news: Error cause parameter (ES2022) is widely supported:

  • Available since September 2021 (4+ years)
  • Foundry V13 minimum: Chromium 122, Firefox 127, Electron 33+
  • Safe to use in all Foundry V13+ environments

Usage:

const error = err instanceof Error ? err : new Error(String(err), { cause: err });

Preserves original error context for debugging in modern browsers.


Implementation Checklist

When adding error handling to your module:

1. Audit All Catch Blocks

grep -r "} catch" scripts/

2. Classify Each Error

  • Unexpected failure? → Pattern 1 (user-facing)
  • Expected validation? → Pattern 2 (expected validation)
  • Diagnostic only? → Pattern 3 (developer-only)
  • High-frequency? → Pattern 4 (throttled)

3. Replace Patterns

  • console.logconsole.error or Hooks.onError (minimum fix)
  • Manual notification + console → Hooks.onError (notify: null) + ui.notifications.*
  • Expected failures → ui.notifications.warn with console: false
  • Use { clean: true } on ui.notifications.* for user/document data
  • Default to console: false (prevents noise + double-logging)
  • Never put untrusted strings in Hooks.onError msg (it's a prefix only)
  • Always separate error funnel from user notification
  • Preserve original error: new Error(String(err), { cause: err })
  • Add structured data to Hooks.onError for debugging
  • Throttle errors in render loops/hooks (prevent UI spam)

4. Test Error Paths

  • Verify user-facing errors show clean message (no technical details leaked)
  • Verify stack traces in console (Error objects via Hooks.onError)
  • Verify warn severity used for expected validation errors
  • Check that diagnostic errors don't spam UI (Hooks.onError with notify: null)
  • Test throttling for high-frequency error paths (5 second window)
  • Verify no double-logging (Hooks.onError + ui.notifications with console: false)
  • Verify structured data appears in error hooks for debugging

Verification Notes

Verified against Foundry V13 API (2025-12-31):

✅ Documented and Correct

  • NotificationOptions.console: Optional boolean - "Whether to log the message to the console"
  • NotificationOptions.clean: Optional boolean - "Whether to clean the provided message string as untrusted user input"
  • Hooks.onError signature:
    static onError(
      location: string,
      error: Error,
      options?: {
        data?: object;
        log?: null | string;
        msg?: string;
        notify?: null | string;
      }
    ): void
    
  • All four parameters documented:
    • msg: String - "A message which should prefix the resulting error or notification"
    • log: null | string - "The level at which to log the error to console (if at all)"
    • notify: null | string - "The level at which to spawn a notification in the UI (if at all)"
    • data: object - "Additional data to pass to the hook subscribers"
  • Notification types: "info", "warn", "error" (all documented)
  • notify: null: Explicitly documented as valid (suppresses UI notification)
  • { cause: err }: Excellent browser support (ES2022, 4+ years available)

⚠️ Undocumented Behaviors

  • console default value not explicitly stated (defensive console: false recommended)
  • notify string values not enumerated (use notify: null + separate notifications)
  • Hooks.onError mutates error.message (normalize to Error objects, separate funnel from notification)

Benefits of This Approach

  • Foundry-native: Uses only documented NotificationOptions and Hooks.onError APIs
  • Full UX control: Always separates error funnel from user message (no technical leaks)
  • Stack traces: Error objects via Hooks.onError include full stack traces in console
  • Preserves context: { cause: err } retains original error when wrapping non-Errors
  • Structured debugging: data parameter provides context to hook subscribers and debugging
  • Ecosystem-compatible: Error funnel allows other modules to listen to your errors
  • Better UX: Severity discipline (warn vs error) + throttling prevents notification spam
  • Sanitization: { clean: true } on ui.notifications.* prevents XSS from error messages
  • No double-logging: Explicit console: false prevents duplicate entries
  • Reduced console noise: Expected validation errors don't clutter console by default
  • Consistent pattern: All errors use same "funnel + notification" approach
  • Robust throttling: Module-scoped Map prevents spam across multiple instances
  • Future-proof: Uses only documented Foundry V13 error handling mechanisms

Optional Enhancement: Localization

For published modules, consider localizing error messages:

const message = game.i18n.format("YOUR_MODULE.Error.FailedToAddItem", {
  error: err.message
});
ui.notifications.error(message, { clean: true, console: false });

Note: If using game.i18n.format, the format function returns a sanitized string, so clean: true may be redundant (but harmless).


References

Foundry V13 API Documentation

GitHub Issues

Browser APIs

Related Skills

  • foundry-vtt-performance-safe-updates - Multi-client update safety patterns
  • foundry-vtt-dialog-compat - DialogV2 Shadow DOM patterns
  • foundry-vtt-version-compat - API compatibility layer patterns

Example: Real-World Usage

// In blades-alternate-actor-sheet.js
import { queueUpdate } from "./lib/update-queue.js";

async _onItemCreate(event) {
  event.preventDefault();
  const itemType = event.currentTarget.dataset.itemType;

  try {
    // Attempt to create item
    const itemData = {
      name: `New ${itemType}`,
      type: itemType,
      system: {}
    };

    await queueUpdate(async () => {
      await this.actor.createEmbeddedDocuments("Item", [itemData]);
    });

    ui.notifications.info(`[BitD-Alt] Created new ${itemType}`);

  } catch (err) {
    // Pattern 1: User-facing failure
    const error = err instanceof Error ? err : new Error(String(err), { cause: err });

    Hooks.onError("BitD-Alt.ItemCreate", error, {
      msg: "[BitD-Alt]",
      log: "error",
      notify: null,
      data: { itemType, actorId: this.actor.id }
    });

    ui.notifications.error(`[BitD-Alt] Failed to create ${itemType}`, {
      clean: true,
      console: false
    });
  }
}

Last Updated: 2025-12-31 Status: Production-Ready (Verified against Foundry V13 API) Maintainer: Claude Code (BitD Alternate Sheets)