<!--
title: Fail-safe Error Handling
domain: ai-agents
topic: Error Handling
language: TypeScript
source: QwenLM/qwen-code
updated: 2026-07-28
url: https://awesomereviewers.com/reviewers/qwen-code-fail-safe-error-handling/
-->

Use fail-safe error handling everywhere in long-running / retrying flows: cleanup must be guaranteed, state/cursors must follow the intended retry semantics, and errors must remain diagnosable.

Apply these rules:
1) Cleanup in finally, not best-effort.
- For every resource lifecycle (server, sockets, file handles, event subscriptions), ensure close/dispose happens in a try/finally even when other operations throw.

2) Decide per error kind: retry, terminal-skip, or bubble.
- If failure is retryable: do not advance cursors or dedup keys; rethrow so the poll loop retries.
- If failure is terminal (e.g., 404/410 deleted/transfer): skip only that item and advance so you don’t wedge the batch.
- If you can’t guarantee either, at least record enough telemetry and avoid infinite loops.

3) Never “silently succeed” after a failed dispatch.
- If a delivery/dispatch fails, ensure the lane/process doesn’t mark the notification/comment as handled in a way that prevents retries, or it will permanently lose messages.
- Conversely, if you want at-most-once semantics, record only on successful dispatch.

4) Preserve diagnostics.
- When wrapping errors, preserve the cause: `throw new X(message, { cause: error })`.
- Use structured/typed errors (e.g., EXECUTION_DENIED vs UNHANDLED_EXCEPTION) so telemetry and fallback logic behave correctly.

5) Classify timeouts/cancellations/quotas correctly.
- Treat “quota exhausted” as fast-fail without re-triggering generic rate-limit retry loops.
- Treat user cancellation distinctly from “unavailable picker”/display errors.

Example pattern (cursor + fail-safe dispatch):

```ts
async function processBatch(items: Item[], cursor: Cursor) {
  let firstError: unknown;
  for (const item of items) {
    try {
      const ok = await dispatch(item);
      if (ok) cursor.markHandled(item.id);
      // advance cursor for success (or terminal skip)
    } catch (err) {
      if (isTerminal(err)) {
        // skip permanently but advance cursor so you don’t wedge
        cursor.markHandled(item.id);
        continue;
      }
      // retryable: do NOT advance cursor/dedup keys
      firstError ??= err;
    }
  }
  if (firstError) throw firstError;
  cursor.advanceToLatest();
  await cursor.save();
}

function isTerminal(err: unknown): boolean {
  // 404/410 patterns; also treat auth/permission without retry-after as terminal
  return err instanceof HttpError && (err.status === 404 || err.status === 410);
}
```

This standard prevents the common failure modes seen across the discussions: orphaned state from missing cleanup, lost messages from swallowing/“marking handled” on failed dispatch, infinite poll livelocks from not advancing cursor, and operator-blind failures from lost error context.
