Skip to content
Back to skills

Fvtt Error Handling

ASecurity

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.

  • 666 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added February 8, 2026
developmentjavascripttypescriptrustgojavabashdebugginggitapiperformance

Works with

  • claude code
  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill fvtt-error-handling --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Fvtt Error Handling?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Fvtt Error Handling
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-fvtt-error-handling/badge)](https://www.skillsdirectory.com/skills/majiayu000-fvtt-error-handling)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
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

```javascript
} 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

```javascript
} 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:
  ```javascript
  console: game.settings.get("your-module", "enableProfiling")
  ```

---

## Pattern 3: Developer-Only Errors

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

```javascript
} 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

```javascript
// 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:

```javascript
// 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:**
```javascript
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:
```javascript
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:
```javascript
// 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 }`

```javascript
// ❌ 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**:
```javascript
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
```bash
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**:
  ```typescript
  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:

```javascript
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
- [NotificationOptions Interface](https://foundryvtt.com/api/interfaces/foundry.NotificationOptions.html)
- [Hooks.onError Method](https://foundryvtt.com/api/classes/foundry.helpers.Hooks.html)
- [Notifications Class](https://foundryvtt.com/api/classes/foundry.applications.ui.Notifications.html)

### GitHub Issues
- [Hooks.onError Implementation #6669](https://github.com/foundryvtt/foundryvtt/issues/6669) - Documents error.message mutation behavior

### Browser APIs
- [Error.cause - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause)
- [Can I Use: Error.cause](https://caniuse.com/mdn-javascript_builtins_error_cause)

### 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

```javascript
// 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)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.