# Tool handlers catch errors and return isError: true

- **Pattern:** `ab-001841` (`mcp-server.error-resilience.tool-error-catch`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001841
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001841)

## Why it matters

MCP tools that let exceptions propagate unhandled either crash the server process or produce an opaque JSON-RPC internal error (`-32603`) with no actionable detail. CWE-703 (improper check of exceptional conditions) is the root; CWE-209 (generation of error messages with sensitive information) is the follow-on risk when raw stack traces leak internal file paths and dependency versions into tool output. The correct MCP pattern is to catch exceptions inside the handler and return `isError: true` with a descriptive, scrubbed message — keeping the server alive and giving the AI something useful to act on.

## Severity rationale

High because unhandled exceptions crash the server or surface raw stack traces, taking down all tools simultaneously and potentially exposing internal implementation details.

## Remediation

Wrap every tool handler body in a try/catch. Return `isError: true` with a clean error message — never rethrow.

```ts
// src/tools/query.ts
server.tool('query_db', ..., async ({ sql }) => {
  try {
    const result = await db.query(sql)
    return { content: [{ type: 'text', text: JSON.stringify(result) }] }
  } catch (error) {
    // Return error as content — do not rethrow
    return {
      content: [{ type: 'text', text: `Query failed: ${(error as Error).message}` }],
      isError: true,
    }
  }
})
```

Strip stack traces before including error messages in the response. Expose the error type and message, not the stack or internal paths.

## Detection

- **ID:** `tool-error-catch`
- **Severity:** `high`
- **What to look for:** Count all tool handler functions. Enumerate which have try/catch blocks or error boundaries vs. which let exceptions propagate unhandled. Examine every tool handler function. Check that each handler has try/catch (or equivalent error handling). When an error occurs inside a tool handler, the MCP pattern is to return a normal result with `isError: true` and the error message in the content — not to throw an unhandled exception (which would crash the server or produce a JSON-RPC error). Check that error messages are user-readable, not raw stack traces.
- **Pass criteria:** Every tool handler has error handling that catches exceptions and returns `isError: true` with a descriptive error message in the content array. No unhandled exceptions escape tool handlers. 100% of tool handlers must have error handling that catches exceptions and returns structured errors.
- **Fail criteria:** Tool handlers have no try/catch (exceptions crash the server), or errors are caught but re-thrown as JSON-RPC errors instead of returned as `isError: true`, or error messages expose raw stack traces.
- **Skip (N/A) when:** The server registers no tools. All checks skip when no MCP server is detected.
- **Do NOT pass when:** Tool handlers catch errors but return them as plain text strings instead of structured MCP error responses.

- **Cross-reference:** For structured error format, see `structured-errors`. For malformed input handling, see `malformed-input`.
- **Detail on fail:** `"4 of 6 tool handlers have no try/catch — any error will crash the server or produce an opaque JSON-RPC error instead of a readable message"` or `"Tool 'query_db' catches errors but returns the full stack trace in content — exposes internal paths and implementation details"`
- **Remediation:** Tool errors should be returned as content, not thrown:

  ```ts
  // src/tools/search.ts — tool with error handling
  try { const results = await search(args.query); return { content: [{ type: "text", text: JSON.stringify(results) }] } }
  catch (e) { return { content: [{ type: "text", text: "Error: " + e.message }], isError: true } }
  ```

  ```ts
  server.tool('query_db', ..., async ({ sql }) => {
    try {
      const result = await db.query(sql)
      return { content: [{ type: 'text', text: JSON.stringify(result) }] }
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Query failed: ${error.message}` }],
        isError: true,
      }
    }
  })
  ```

  ```python
  @mcp.tool()
  async def query_db(sql: str) -> str:
      try:
          result = await db.execute(sql)
          return json.dumps(result)
      except Exception as e:
          # Return error as content, not as exception
          raise McpError(ErrorCode.InternalError, f"Query failed: {str(e)}")
  ```

## External references

- cwe CWE-703
- cwe CWE-209
- external mcp-spec-tool-iserror — https://modelcontextprotocol.io/docs/concepts/tools#tool-results

Taxons: error-resilience

HTML version: https://auditbuffet.com/patterns/ab-001841
