Claude
Skills
Sign in
Back

fvtt-error-handling

Included with Lifetime
$97 forever

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.

Design

What this skill does


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

Related in Design