Claude-skill-registry 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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fvtt-error-handling" ~/.claude/skills/majiayu000-claude-skill-registry-fvtt-error-handling && rm -rf "$T"
skills/data/fvtt-error-handling/SKILL.mdFoundry 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 (
): Logs stack traces, triggers ecosystem hooks, provides structured dataHooks.onError - User notification (
): Clean, sanitized messages with full UX controlui.notifications.*
Why separation matters:
mutatesHooks.onError
whenerror.message
is provided (see Foundry GitHub #6669)msg
only works in NotificationOptions, not Hooks.onErrorclean: true- User sees only your controlled message, not technical details
- Explicit control over console logging (
prevents double-logging)console: false
Quick Reference: Four Error Patterns
| Pattern | Error Funnel | User Notification | Use Case |
|---|---|---|---|
| User-facing | Hooks.onError () | ui.notifications.error | Unexpected failures |
| Expected validation | (none) | ui.notifications.warn | User input errors |
| Developer-only | Hooks.onError () | (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
(no technical details leaked)userFacingDescription - Stack trace logged via Hooks.onError (full Error object)
- Ecosystem visibility (other modules can listen to
)Hooks.on("error", ...) - Structured
for debugging and hook subscribersdata - Explicit
prevents double-loggingconsole: false
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
severity (notwarn
) for expected caseserror - Simpler than Hooks.onError (no error funnel needed for routine validation)
avoids noise for common user-driven failuresconsole: false- 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
for truly isolated cases (but lose ecosystem hooks)console.error(...)
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
severity for non-critical repeated errorswarn - Separates error funnel (logs only) from user notification
- Explicit
prevents double-loggingconsole: false
Decision Tree: Classifying Errors
Ask these questions:
-
Is this unexpected? (system failure, unhandled case, critical error) → Pattern 1: User-facing failures
-
Is this expected validation? (user input error, missing required data) → Pattern 2: Expected validation
-
Is this diagnostic-only? (internal state, no user impact) → Pattern 3: Developer-only
-
Could this fire repeatedly? (render loop, hook, event handler) → Pattern 4: High-frequency throttled
Foundry-Specific Gotchas
1. Hooks.onError
Mutates Error Objects
Hooks.onErrorCritical:
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
consoleIssue: 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
notifyIssue:
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
clean: trueIssue:
{ clean: true } sanitizes untrusted input, but ONLY in ui.notifications.*.
Does NOT work in
: The Hooks.onError
msg parameter is NOT sanitized by Hooks.onError.
Best practice:
- Keep
in Hooks.onError generic (module prefix only):msgmsg: "[YourModule]" - Put user/document data in separate
call withui.notifications.*{ 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
{ cause: err }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
or Hooks.onError (minimum fix)console.error - Manual notification + console → Hooks.onError (
) + ui.notifications.*notify: null - Expected failures →
withui.notifications.warnconsole: false - Use
on ui.notifications.* for user/document data{ clean: true } - Default to
(prevents noise + double-logging)console: false - Never put untrusted strings in Hooks.onError
(it's a prefix only)msg - Always separate error funnel from user notification
- Preserve original error:
new Error(String(err), { cause: err }) - Add structured
to Hooks.onError for debuggingdata - 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
severity used for expected validation errorswarn - 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
appears in error hooks for debuggingdata
Verification Notes
Verified against Foundry V13 API (2025-12-31):
✅ Documented and Correct
: Optional boolean - "Whether to log the message to the console"NotificationOptions.console
: Optional boolean - "Whether to clean the provided message string as untrusted user input"NotificationOptions.clean
signature:Hooks.onErrorstatic onError( location: string, error: Error, options?: { data?: object; log?: null | string; msg?: string; notify?: null | string; } ): void- All four parameters documented:
: String - "A message which should prefix the resulting error or notification"msg
: null | string - "The level at which to log the error to console (if at all)"log
: null | string - "The level at which to spawn a notification in the UI (if at all)"notify
: object - "Additional data to pass to the hook subscribers"data
- Notification types:
,"info"
,"warn"
(all documented)"error"
: Explicitly documented as valid (suppresses UI notification)notify: null
: Excellent browser support (ES2022, 4+ years available){ cause: err }
⚠️ Undocumented Behaviors
default value not explicitly stated (defensiveconsole
recommended)console: false
string values not enumerated (usenotify
+ separate notifications)notify: null
mutatesHooks.onError
(normalize to Error objects, separate funnel from notification)error.message
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:
retains original error when wrapping non-Errors{ cause: err } - Structured debugging:
parameter provides context to hook subscribers and debuggingdata - Ecosystem-compatible: Error funnel allows other modules to listen to your errors
- Better UX: Severity discipline (warn vs error) + throttling prevents notification spam
- Sanitization:
on ui.notifications.* prevents XSS from error messages{ clean: true } - No double-logging: Explicit
prevents duplicate entriesconsole: false - 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
- Hooks.onError Implementation #6669 - Documents error.message mutation behavior
Browser APIs
Related Skills
- Multi-client update safety patternsfoundry-vtt-performance-safe-updates
- DialogV2 Shadow DOM patternsfoundry-vtt-dialog-compat
- API compatibility layer patternsfoundry-vtt-version-compat
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)