Install
$ agentstack add skill-techymt-claude-code-superpowers-error-handling ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Error Handling
The pattern
Claude Code has two error categories. Session errors are unrecoverable failures that crash the process or abort the current session (network configuration issues, authentication failures, corrupted state). Tool errors are expected failures that should be shown to the LLM so it can reason about them, retry, or explain the situation to the user.
The fundamental rule: tool.call() throws typed errors for expected failures. The framework catches them, calls formatError(error) from src/utils/toolErrors.ts, and delivers the formatted string to the LLM as a tool_result block with is_error: true. Only truly unrecoverable infrastructure failures should propagate past the framework as unhandled exceptions.
Why this matters
The LLM is in a conversation loop. If a tool throws a raw, unformatted exception, the framework must decide what to show the model — a stack trace is not useful. By throwing typed errors (ShellError, AbortError, Error), tools hand the framework enough structure to produce a clean, readable message. For example, a file-not-found error thrown from FileReadTool lets the LLM suggest the correct path or ask the user to check.
This also means the user sees a coherent explanation rather than an error box. The LLM translates the technical error into natural language in its next response.
Custom error types (ShellError, AbortError) are used at system boundaries — they carry structured context (exit code, stderr, interrupted flag) that formatError uses to build the right message.
How to apply it
- Wrap all I/O in try-catch inside
call(). Catch specific error types first, then rethrow or let them propagate. throw new Error(message)for expected, non-shell failures. Include enough context for the LLM to act: what was attempted, what failed, what the user could check.- For ENOENT (file not found): use
isENOENT()fromutils/errors.jsto detect it and thrownew Error("File not found: /path. Does it exist?"). - For command failures: throw
new ShellError(stdout, stderr, code, interrupted)—formatErrorassembles exit code and stderr automatically. - For user cancellation: throw
new AbortError()—formatErrormaps it to a clean interruption message. - Define custom error types when an error carries structured data that formatting logic needs.
In the source
// Source: src/tools/BashTool/BashTool.tsx
if (result.preSpawnError) {
throw new Error(result.preSpawnError) // pre-spawn failures → plain Error
}
if (interpretationResult.isError && !isInterrupt) {
throw new ShellError('', outputWithSbFailures, result.code, result.interrupted)
// ShellError carries structured context; formatError builds the LLM message
}
// Source: src/utils/toolErrors.ts — framework calls this after catching
export function formatError(error: unknown): string {
if (error instanceof AbortError) {
return error.message || INTERRUPT_MESSAGE_FOR_TOOL_USE
}
if (!(error instanceof Error)) return String(error)
const parts = getErrorParts(error)
return parts.filter(Boolean).join('\n').trim() || 'Command failed with no output'
// truncated to 10 000 chars if longer
}
export function getErrorParts(error: Error): string[] {
if (error instanceof ShellError) {
return [`Exit code ${error.code}`, error.interrupted ? '...' : '', error.stderr, error.stdout]
}
return [error.message]
}
// Source: src/tools/FileReadTool/FileReadTool.ts
import { isENOENT } from '../../utils/errors.js' // NOT utils/file.js
} catch (error) {
if (isENOENT(error)) {
throw new Error(`File not found: ${file_path}. Does it exist?`)
}
throw error // re-throw unknown errors — framework handles them
}
The abort-error branch is non-obvious: when the user presses Ctrl+C, an AbortError propagates through all in-flight async calls. Tools must let it propagate (or throw a new AbortError) so formatError returns a clean "interrupted" message.
Apply it to your code
Before — tool that returns a wrong shape instead of throwing:
async call(args, context, canUseTool) {
try {
const response = await fetch(args.url)
if (!response.ok) {
return { type: 'error', error: `HTTP ${response.status}` } // Wrong shape: ToolResult has no error variant
}
return { type: 'success', data: await response.text() } // Wrong shape: no 'type' field on ToolResult
} catch (err) {
return { type: 'error', error: String(err) } // Wrong: swallows AbortError, wrong shape
}
}
After — tool that throws typed errors:
async call(args, context, canUseTool) {
try {
const response = await fetch(args.url, {
signal: context.abortController.signal, // WHY: lets AbortError propagate on Ctrl+C
})
if (!response.ok) {
// WHY: throw Error so formatError surfaces a readable message to the LLM
throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${args.url}`)
}
return { data: await response.text() } // WHY: ToolResult is just { data: T }
} catch (err) {
if (err instanceof AbortError) throw err // WHY: let framework handle cancellation cleanly
// WHY: network errors (DNS failure, timeout) re-thrown as plain Error with context
throw new Error(`Network error fetching ${args.url}: ${err instanceof Error ? err.message : String(err)}`)
}
}
Signals that you need this pattern
- Tool's
call()returns{ type: 'error', error: ... }— there is no error variant onToolResult; throw instead - Unhandled promise rejections appearing in logs from tool execution
- The LLM receives a raw stack trace as tool result content
- Error messages say "something went wrong" without a file path, exit code, or actionable hint
isENOENTimported fromutils/file.js— the correct source isutils/errors.jsAbortErroris caught and swallowed — Ctrl+C produces an error dialog instead of a clean cancellation
Signals that you're over-applying it
- Utilities called only within other tools don't need special error handling — they can throw freely; the calling tool catches
- Session-level errors (authentication failure, Claude API unreachable) should propagate as exceptions to the session handler, not be swallowed as tool errors
- Don't write exhaustive catch branches for impossible error cases from internal calls you control
Works with
tool-definition— the ToolResult type and where errors appear in the return valuepermission-system— handling permission denial as a specific error caseasync-concurrency— AbortError handling when tools are cancelled
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: TechyMT
- Source: TechyMT/claude-code-superpowers
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.