Companion to the ## Error Messages section of CLAUDE.md. That section holds the rules. This file holds longer examples and anti-patterns that would bloat CLAUDE.md inlined.
Every message needs, in order:
- What — the rule that was broken.
- Where — the exact file, line, key, field, or CLI flag.
- Saw vs. wanted — the bad value and the allowed shape or set.
- Fix — one concrete action, in imperative voice.
Callers may match on the message text, so stability matters. Aim for one sentence.
| ✗ / ✓ | Message | Notes |
|---|---|---|
| ✗ | Error: invalid component |
No rule, no saw, no where. |
| ✗ | The "name" component of type "npm" failed validation because the provided value "" is empty, which is not allowed because names are required; please provide a non-empty name. |
Restates the rule three times. |
| ✓ | npm "name" component is required |
Rule + where + implied saw (missing). Six words. |
| ✗ | Error: bad name |
No rule. |
| ✓ | name "__proto__" cannot start with an underscore |
Rule, where (name), saw (__proto__), fix implied. |
| ✗ | Error: invalid argument |
No where, no rule, no fix. |
| ✓ | orgSlug is required |
Rule + where (orgSlug), saw (missing), implies fix. |
| ✗ | Error: request failed |
No status, no hint what to check. |
| ✓ | Socket API rejected the token (401); check SOCKET_API_KEY |
Rule (401), where (token), fix (check env var). |
The reader is looking at a file and wants to fix the record without re-running the tool. Give each ingredient its own words.
✗ Error: invalid tour config
✓ tour.json: part 3 ("Parsing & Normalization") is missing "filename". Add a single-word lowercase filename (e.g. "parsing") to this part. One per part is required to route /<slug>/part/3 at publish time.
Breakdown:
- What:
is missing "filename". The rule is "each part has a filename". - Where:
tour.json: part 3 ("Parsing & Normalization"). File + record + human label. - Saw vs. wanted: saw = missing; wanted = a single-word lowercase filename, with
"parsing"as a concrete model. - Fix:
Add … to this part. Imperative, specific.
The trailing to route /<slug>/part/3 at publish time is optional. Include a why clause only when the rule is non-obvious. Skip it for rules the reader already knows (e.g. "names can't start with an underscore").
Internal assertions and invariant checks. No end user will read them. Terse keeps the assertion readable when you skim the code.
- ✓
assert(queue.length > 0)with messagequeue drained before worker exit - ✓
pool size must be positive - ✗
An unexpected error occurred while trying to acquire a connection from the pool because the pool size was not positive.Nothing a maintainer can act on that the rule itself doesn't already say.
"Invalid X" with no rule.
- ✗
Invalid filename 'My Part' - ✓
filename 'My Part' must be [a-z]+ (lowercase, no spaces)
Passive voice on the fix.
- ✗
"filename" was missing - ✓
add "filename" to part 3
Naming only one side of a collision.
- ✗
duplicate key "foo"(which record won, which lost?) - ✓
duplicate key "foo" in config.json (lines 12 and 47); rename one
Silently auto-correcting.
- ✗ Stripping a trailing slash from a URL and continuing. The next run will hit the same bug. Nothing learned.
- ✓
url "https://api/" has a trailing slash; remove it.
Bloat that restates the rule.
- ✗
The value provided for "timeout" is invalid because timeouts must be positive numbers and the value you provided was not a positive number. - ✓
timeout must be a positive number (saw: -5)
When the error needs to show an allowed set, a list of conflicting
records, or multiple missing fields, use the list formatters from
@socketsecurity/lib/arrays rather than hand-joining with commas:
joinAnd(['a', 'b', 'c'])→"a, b, and c"for conjunctions ("missing foo, bar, and baz")joinOr(['npm', 'pypi', 'maven'])→"npm, pypi, or maven"for disjunctions ("must be one of: …")
Both wrap Intl.ListFormat, so the Oxford comma and one-/two-item cases come out right for free (joinOr(['a']) → "a"; joinOr(['a', 'b']) → "a or b").
- ✗
--reach-ecosystems must be one of: npm, pypi, maven (saw: "foo"). Hand-joined; breaks if the list has one or two entries. - ✓
`--reach-ecosystems must be one of: ${joinOr(ALLOWED)} (saw: "foo")` - ✗
missing keys: filename slug title. No separators, no grammar. - ✓
`missing keys: ${joinAnd(missing)}`→"missing keys: filename, slug, and title"
Use joinOr whenever the error is "must be one of X", joinAnd whenever it's "all of X are required / missing / in conflict".
catch (e) binds unknown. The helpers in @socketsecurity/lib/errors cover the four patterns that recur:
import {
errorMessage,
errorStack,
isError,
isErrnoException,
} from '@socketsecurity/lib/errors'Cross-realm-safe. Uses the native ES2025 Error.isError when the engine ships it, falls back to a spec-compliant shim otherwise. Catches Errors from worker threads, vm contexts, and iframes that same-realm instanceof Error misses.
- ✗
if (e instanceof Error) { … } - ✓
if (isError(e)) { … }
Narrows to NodeJS.ErrnoException (an Error with a string code set by libuv/syscalls like ENOENT, EACCES, EBUSY, EPERM). Builds on isError, so it's also cross-realm-safe. It checks that code is a string. A branded Error without a real errno code returns false.
- ✗
if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') { … } - ✓
if (isErrnoException(e) && e.code === 'ENOENT') { … }
Walks the cause chain via messageWithCauses, coerces primitives and objects to string, and returns the shared UNKNOWN_ERROR sentinel (the string 'Unknown error') for null, undefined, empty strings, [object Object], or Errors with no message.
That last bullet is the important one: every || 'Unknown error' fallback in the fleet should collapse into a single errorMessage(e) call.
- ✗
`Failed: ${e instanceof Error ? e.message : String(e)}` - ✗
`Failed: ${(e as Error)?.message ?? 'Unknown error'}` - ✗
`Failed: ${e instanceof Error ? e.message : 'Unknown error'}` - ✓
`Failed: ${errorMessage(e)}`
When you want to preserve the cause chain upstream (recommended), pair it with { cause }:
try {
await readConfig(path)
} catch (e) {
throw new Error(`Failed to read ${path}: ${errorMessage(e)}`, { cause: e })
}Returns the cause-walking stack for Errors. Returns undefined for non-Errors so logger calls stay safe:
logger.error(`rebuild failed: ${errorMessage(e)}`, { stack: errorStack(e) })- Imperative for the fix:
rename,add,remove,set. - Present tense for the rule:
must be,cannot,is required. - No apology ("Sorry, …"), no blame ("You provided …"). State the rule and the fix.
- Don't end with "please". It adds no information and makes the message feel longer than it is.
Before shipping a message, cross out any word that, if removed, leaves the information intact. If only rhythm or politeness disappears, drop it.