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.
Adds production-ready error handling patterns for Foundry VTT modules using Hooks.onError and NotificationOptions. Claude will use this when adding catch blocks, standardizing error handling, or ensuring proper UX with user messages vs technical logs.
/plugin marketplace add ImproperSubset/hh-agentics/plugin install fvtt-dev@hh-agenticsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Domain: Foundry VTT V12/V13 Error Handling Status: Production-Ready (Verified against V13 API) Last Updated: 2025-12-31
This skill provides production-ready error handling patterns for Foundry VTT modules, using the documented V13 APIs: NotificationOptions and Hooks.onError.
Always separate error funnel from user notification:
Hooks.onError): Logs stack traces, triggers ecosystem hooks, provides structured dataui.notifications.*): Clean, sanitized messages with full UX controlWhy separation matters:
Hooks.onError mutates error.message when msg is provided (see Foundry GitHub #6669)clean: true only works in NotificationOptions, not Hooks.onErrorconsole: false prevents double-logging)| 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 |
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:
userFacingDescription (no technical details leaked)Hooks.on("error", ...))data for debugging and hook subscribersconsole: false prevents double-loggingUse 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:
warn severity (not error) for expected casesconsole: false avoids noise for common user-driven failuresconsole: game.settings.get("your-module", "enableProfiling")
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:
console.error(...) for truly isolated cases (but lose ecosystem hooks)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:
warn severity for non-critical repeated errorsconsole: false prevents double-loggingAsk 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
Hooks.onError Mutates Error ObjectsCritical: 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:
Always normalize to Error objects:
const error = err instanceof Error ? err : new Error(String(err), { cause: err });
console Default Behavior Not DocumentedIssue: 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
});
notify String Values Are UndocumentedIssue: 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
clean: true Only Works in NotificationOptionsIssue: { 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:
msg in Hooks.onError generic (module prefix only): msg: "[YourModule]"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 });
{ cause: err } Has Excellent SupportGood news: Error cause parameter (ES2022) is widely supported:
Usage:
const error = err instanceof Error ? err : new Error(String(err), { cause: err });
Preserves original error context for debugging in modern browsers.
When adding error handling to your module:
grep -r "} catch" scripts/
console.log → console.error or Hooks.onError (minimum fix)notify: null) + ui.notifications.*ui.notifications.warn with console: false{ clean: true } on ui.notifications.* for user/document dataconsole: false (prevents noise + double-logging)msg (it's a prefix only)new Error(String(err), { cause: err })data to Hooks.onError for debuggingwarn severity used for expected validation errorsnotify: null)console: false)data appears in error hooks for debuggingVerified against Foundry V13 API (2025-12-31):
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
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""info", "warn", "error" (all documented)notify: null: Explicitly documented as valid (suppresses UI notification){ cause: err }: Excellent browser support (ES2022, 4+ years available)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){ cause: err } retains original error when wrapping non-Errorsdata parameter provides context to hook subscribers and debugging{ clean: true } on ui.notifications.* prevents XSS from error messagesconsole: false prevents duplicate entriesFor 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).
foundry-vtt-performance-safe-updates - Multi-client update safety patternsfoundry-vtt-dialog-compat - DialogV2 Shadow DOM patternsfoundry-vtt-version-compat - API compatibility layer patterns// 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)
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.