# Error handling and boundary tests

- **Pattern:** `ab-002146` (`code-quality-essentials.testing.error-handling`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002146
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002146)

## Why it matters

A test suite that only verifies the happy path does not test the code that actually runs when things go wrong. ISO 25010:2011 §6.5.5 requires reliability verification to cover failure modes. Missing error-path tests mean: API 404 and 500 responses produce untested UI states that may crash or show stale data; invalid inputs reach business logic that was never verified to reject them correctly; `catch` blocks that silently swallow errors pass code review because no test ever triggered them.

## Severity rationale

Low because untested error paths do not break current functionality but guarantee that failure scenarios — which happen in production — have never been automatically verified.

## Remediation

Add at least one negative test per critical unit. A minimal pattern alongside each success test:

```typescript
describe('fetchUser', () => {
  it('returns user data on success', async () => {
    mockFetch.mockResolvedValue({ id: '1', name: 'Alice' })
    const user = await fetchUser('1')
    expect(user.name).toBe('Alice')
  })

  it('throws UserNotFoundError when 404', async () => {
    mockFetch.mockRejectedValue(new Response(null, { status: 404 }))
    await expect(fetchUser('nonexistent')).rejects.toBeInstanceOf(UserNotFoundError)
  })

  it('handles null user id gracefully', async () => {
    await expect(fetchUser(null as unknown as string))
      .rejects.toThrow('User ID is required')
  })
})
```

Aim for at least 20% of tests to verify failure scenarios, not just success paths.

## Detection

- **ID:** `error-handling`
- **Severity:** `low`
- **What to look for:** Examine the test suite (if it exists) for tests that verify error scenarios, not just success paths. Look for: tests that pass invalid input to functions and assert the error thrown; tests that simulate API failures (network errors, 4xx/5xx responses) and assert the UI shows an error state; React Error Boundary tests; tests for null/empty/boundary values (empty arrays, zero, empty string, undefined). Also check the production code for the presence of `try/catch` blocks in async operations — missing error handling is a separate issue but correlated. A codebase with 100% "happy path" tests and no failure scenario tests is brittle.
- **Pass criteria:** Enumerate all relevant code locations. Tests exist that cover failure scenarios — invalid input, network errors, null/empty edge cases. Error boundaries (if React) are tested. At least 20% of tests verify failure paths.
- **Fail criteria:** All tests only cover success scenarios; no tests assert error states, throws, or empty results.
- **Skip (N/A) when:** No test files exist.
- **Detail on fail:** `"All tests cover success paths only; no tests for invalid input, API failures, or null/empty edge cases"`
- **Remediation:** Add negative tests alongside each unit test. A minimal pattern:

  ```typescript
  describe('fetchUser', () => {
    it('returns user data on success', async () => {
      mockFetch.mockResolvedValue({ id: '1', name: 'Alice' })
      const user = await fetchUser('1')
      expect(user.name).toBe('Alice')
    })

    it('throws a UserNotFoundError when 404', async () => {
      mockFetch.mockRejectedValue(new Response(null, { status: 404 }))
      await expect(fetchUser('nonexistent')).rejects.toBeInstanceOf(UserNotFoundError)
    })

    it('handles null user id gracefully', async () => {
      await expect(fetchUser(null as unknown as string))
        .rejects.toThrow('User ID is required')
    })
  })
  ```

## External references

- iso-25010 maintainability.testability

Taxons: code-quality

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