Skip to content
fvtt-error-handling logo

Foundry VTT Error Handling Patterns

fvtt-error-handling

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.

SKILL.md

Full skill instructions

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

PatternError FunnelUser NotificationUse Case
User-facingHooks.onError (notify: null)ui.notifications.errorUnexpected failures
Expected validation(none)ui.notifications.warnUser input errors
Developer-onlyHooks.onError (notify: null)(none)Diagnostic logging
High-frequencyThrottled Hooks.onErrorThrottled ui.notifications.warnRender 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.log → console.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)

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View