copy-x Error Report Dedup Marker
Use when two error handlers sit on the same throw path and each can report to your error tracker (e.g. an inner span/critical-path/retry wrapper that captures-and-rethrows, wrapped by an outer action wrapper that also captures). Tag the Error with a non-enumerable Symbol.for marker so the second handler skips a duplicate event. Triggers on: nested try/catch that both captureException, duplicate issues for one failure, a capture-and-rethrow wrapper, "why are we getting two Sentry events per error".
Install this skill
git submodule add https://github.com/modh-labs/playbook.git .playbookOne submodule installs all 17 skills. Reference .playbook/skills/error-report-dedup/SKILL.md from your AGENTS.md.
Error Report Dedup Marker
When an error passes through two handlers that each report it, your error tracker logs the same failure twice — noisy issues, inflated counts, double alerts. This happens whenever a capture-and-rethrow wrapper (span tracker, critical-path timer, retry helper) is nested inside an outer catch that also captures.
When this activates
- A capture-and-rethrow wrapper nested inside an outer catch that also captures.
- Duplicate issues in Sentry/Bugsnag/Rollbar for a single failure.
- Any "report once, no matter how many handlers see it" requirement.
The pattern
Mark the Error object once; check the mark before reporting.
// error-reported.ts
const REPORTED = Symbol.for("myapp.errorReported");
export function markReported(e: Error): void {
Object.defineProperty(e, REPORTED, {
value: true,
enumerable: false, // never serializes into logs / JSON / the tracker payload
configurable: true,
writable: true,
});
}
export function wasReported(e: Error): boolean {
return (e as unknown as Record<symbol, unknown>)[REPORTED] === true;
}
Producer (inner wrapper):
catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
markReported(err);
reportToTracker(err);
throw err; // ← rethrow the MARKED err, not the original `error`
}
Consumer (outer wrapper):
catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
if (!wasReported(err)) {
markReported(err);
reportToTracker(err);
}
return genericResponse;
}
Core rules
- Non-enumerable +
Symbol.for. Non-enumerable so the marker never serializes into logs, JSON, or the tracker payload.Symbol.for(the global registry) so it's the same symbol across module instances/bundles. - The marker is identity-bound — rethrow the SAME object. This is THE subtle bug. If the producer normalizes a non-Error throw into a fresh Error, marks THAT, but then does
throw error(the original raw value), the marker is lost. The outer handler re-normalizes into yet another Error with no marker and reports again. Always rethrow the normalized, marked Error. - Mark before reporting, so a re-entrant or concurrent path sees it.
Anti-patterns
- A plain field (
error.__reported = true) — enumerable, leaks into serialized payloads and can confuse the tracker's grouping. - A module-local
Symbol()— different module instances don't share it, so the check silently always misses. throw errorafter marking a different normalized object — the headline bug above.- Deduping by message/stack string — fragile and collides across distinct failures.
Test both sides
- Consumer honors a pre-marked error (no second report).
- Producer marks AND rethrows the marked error — including the non-Error-throw case: assert the rethrown value is an Error and
wasReportedis true. Teams forget the producer test, and without it, deleting the producer'smarkstays green while prod double-reports.
Audit checklist
- Marker is a non-enumerable
Symbol.for. - Producer rethrows the marked (normalized) Error, not the raw original.
- Both producer and consumer have tests, including a non-Error throw.
Cross-references
surface-upstream-errors— don't swallow the real error while deduping.control-flow-exceptions— rethrow framework control-flow signals (e.g. Next.jsunstable_rethrow) as the FIRST line of the catch, before any capture or dedup.observability— error-tracker setup and grouping.
Related Skills
PII Redaction Egress Parity
Use when an app sends errors/logs/traces to more than one sink (Sentry events, spans, and logs; a structured logger like pino/winston; an APM). Enforce ONE sensitive-key list across EVERY egress path, and watch structured-logger key-path shapes — a flat dotted key like record["user.email"] is NOT redacted by the path "user.email" in pino (it reads that as nested user→email). Triggers on: adding a log/trace sink, declaring a PII posture ("id + org only"), writing a redact/scrub/beforeSend config, or a "PII showed up in logs even though Sentry scrubbed it" report.
Backend / InfrastructurefingerprintDedup Key Completeness
A de-duplication, idempotency, or uniqueness guard must key on every dimension that makes two records legitimately distinct. A missing dimension silently collapses separate records into one (a lost write with no error). Use when writing an upsert, an onConflict target, a unique constraint, a "have we seen this already?" time window, or any "skip if it exists" guard — and when a bug report says "my second booking/order/ticket disappeared or merged into the first."
Backend / InfrastructureactivityStructured Logging & Observability
Enforce structured logging, Sentry integration, and distributed tracing patterns. Use when adding logging, error tracking, Sentry tags, custom spans, metrics, webhook observability, or debugging production issues. Prevents console.log usage and ensures trace correlation.
Backend / Infrastructure