@webpieces/ai-hook-rules 0.2.114 → 0.2.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,7 +18,7 @@ Both share the same rules and the same `webpieces.ai-hooks.json` config file.
18
18
  ## Install (Claude Code, per project)
19
19
 
20
20
  ```bash
21
- npm install --save-dev @webpieces/webpieces-rules # pulls in ai-hook-rules transitively
21
+ npm install --save-dev @webpieces/nx-webpieces-rules # pulls in ai-hook-rules transitively
22
22
  npx wp-setup-ai-hooks
23
23
  # Restart your Claude Code session
24
24
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/ai-hook-rules",
3
- "version": "0.2.114",
3
+ "version": "0.2.115",
4
4
  "description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -29,7 +29,7 @@
29
29
  "directory": "packages/tooling/ai-hook-rules"
30
30
  },
31
31
  "dependencies": {
32
- "@webpieces/rules-config": "0.2.114"
32
+ "@webpieces/rules-config": "0.2.115"
33
33
  },
34
34
  "publishConfig": {
35
35
  "access": "public"
@@ -1 +1 @@
1
- export declare function writeTemplateIfMissing(workspaceRoot: string, templateName: string): void;
1
+ export { writeTemplateIfMissing } from '@webpieces/rules-config';
@@ -1,18 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.writeTemplateIfMissing = writeTemplateIfMissing;
4
- const tslib_1 = require("tslib");
5
- const fs = tslib_1.__importStar(require("fs"));
6
- const path = tslib_1.__importStar(require("path"));
7
- const INSTRUCT_DIR = '.webpieces/instruct-ai';
8
- function writeTemplateIfMissing(workspaceRoot, templateName) {
9
- const dir = path.join(workspaceRoot, INSTRUCT_DIR);
10
- const filePath = path.join(dir, templateName);
11
- if (fs.existsSync(filePath))
12
- return;
13
- const templatePath = path.join(__dirname, '..', '..', 'templates', templateName);
14
- const content = fs.readFileSync(templatePath, 'utf-8');
15
- fs.mkdirSync(dir, { recursive: true });
16
- fs.writeFileSync(filePath, content);
17
- }
3
+ exports.writeTemplateIfMissing = void 0;
4
+ var rules_config_1 = require("@webpieces/rules-config");
5
+ Object.defineProperty(exports, "writeTemplateIfMissing", { enumerable: true, get: function () { return rules_config_1.writeTemplateIfMissing; } });
18
6
  //# sourceMappingURL=instruct-ai-writer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"instruct-ai-writer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/instruct-ai-writer.ts"],"names":[],"mappings":";;AAKA,wDASC;;AAdD,+CAAyB;AACzB,mDAA6B;AAE7B,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAE9C,SAAgB,sBAAsB,CAAC,aAAqB,EAAE,YAAoB;IAC9E,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAC9C,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO;IAEpC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;IACjF,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACvD,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AACxC,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nconst INSTRUCT_DIR = '.webpieces/instruct-ai';\n\nexport function writeTemplateIfMissing(workspaceRoot: string, templateName: string): void {\n const dir = path.join(workspaceRoot, INSTRUCT_DIR);\n const filePath = path.join(dir, templateName);\n if (fs.existsSync(filePath)) return;\n\n const templatePath = path.join(__dirname, '..', '..', 'templates', templateName);\n const content = fs.readFileSync(templatePath, 'utf-8');\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(filePath, content);\n}\n"]}
1
+ {"version":3,"file":"instruct-ai-writer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/instruct-ai-writer.ts"],"names":[],"mappings":";;;AAAA,wDAAiE;AAAxD,sHAAA,sBAAsB,OAAA","sourcesContent":["export { writeTemplateIfMissing } from '@webpieces/rules-config';\n"]}
@@ -1,11 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const tslib_1 = require("tslib");
4
- const fs = tslib_1.__importStar(require("fs"));
5
- const path = tslib_1.__importStar(require("path"));
6
3
  const types_1 = require("../types");
4
+ const rules_config_1 = require("@webpieces/rules-config");
7
5
  const DEFAULT_LIMIT = 900;
8
- const INSTRUCT_DIR = '.webpieces/instruct-ai';
9
6
  const INSTRUCT_FILE = 'webpieces.filesize.md';
10
7
  const maxFileLinesRule = {
11
8
  name: 'max-file-lines',
@@ -23,109 +20,9 @@ const maxFileLinesRule = {
23
20
  : DEFAULT_LIMIT;
24
21
  if (ctx.projectedFileLines <= limit)
25
22
  return [];
26
- writeInstructionFile(ctx.workspaceRoot);
23
+ (0, rules_config_1.writeTemplateIfMissing)(ctx.workspaceRoot, INSTRUCT_FILE);
27
24
  return [new types_1.Violation(1, `(projected ${String(ctx.projectedFileLines)} lines)`, `File will be ${String(ctx.projectedFileLines)} lines, exceeding the ${String(limit)}-line limit. See .webpieces/instruct-ai/webpieces.filesize.md for detailed refactoring instructions.`)];
28
25
  },
29
26
  };
30
- function writeInstructionFile(workspaceRoot) {
31
- const dir = path.join(workspaceRoot, INSTRUCT_DIR);
32
- const filePath = path.join(dir, INSTRUCT_FILE);
33
- if (fs.existsSync(filePath))
34
- return;
35
- fs.mkdirSync(dir, { recursive: true });
36
- fs.writeFileSync(filePath, FILESIZE_DOC_CONTENT);
37
- }
38
- // eslint-disable-next-line @webpieces/max-file-lines
39
- const FILESIZE_DOC_CONTENT = `# AI Agent Instructions: File Too Long
40
-
41
- **READ THIS FILE to fix files that are too long**
42
-
43
- ## Core Principle
44
-
45
- With **stateless systems + dependency injection, refactor is trivial**.
46
- Pick a method or a few and move to new class XXXXX, then inject XXXXX
47
- into all users of those methods via the constructor.
48
- Delete those methods from original class.
49
-
50
- **99% of files can be less than the configured max lines of code.**
51
-
52
- Files should contain a SINGLE COHESIVE UNIT.
53
- - One class per file (Java convention)
54
- - If class is too large, extract child responsibilities
55
- - Use dependency injection to compose functionality
56
-
57
- ## Command: Reduce File Size
58
-
59
- ### Step 1: Check for Multiple Classes
60
- If the file contains multiple classes, **SEPARATE each class into its own file**.
61
-
62
- ### Step 2: Extract Child Responsibilities (if single class is too large)
63
-
64
- #### Pattern: Create New Service Class with Dependency Injection
65
-
66
- \`\`\`typescript
67
- // BAD: UserController.ts (800 lines, single class)
68
- @provideSingleton()
69
- @Controller()
70
- export class UserController {
71
- // 200 lines: CRUD operations
72
- // 300 lines: validation logic
73
- // 200 lines: notification logic
74
- }
75
-
76
- // GOOD: Extract validation service
77
- // 1. Create UserValidationService.ts
78
- @provideSingleton()
79
- export class UserValidationService {
80
- validateUserData(data: UserData): ValidationResult { /* ... */ }
81
- validateEmail(email: string): boolean { /* ... */ }
82
- }
83
-
84
- // 2. Inject into UserController.ts
85
- @provideSingleton()
86
- @Controller()
87
- export class UserController {
88
- constructor(
89
- @inject(TYPES.UserValidationService)
90
- private validator: UserValidationService
91
- ) {}
92
- }
93
- \`\`\`
94
-
95
- ## AI Agent Action Steps
96
-
97
- 1. **ANALYZE** the file structure:
98
- - Count classes (if >1, separate immediately)
99
- - Identify logical responsibilities within single class
100
-
101
- 2. **IDENTIFY** "child code" to extract:
102
- - Validation logic -> ValidationService
103
- - Notification logic -> NotificationService
104
- - Data transformation -> TransformerService
105
- - External API calls -> ApiService
106
- - Business rules -> RulesEngine
107
-
108
- 3. **CREATE** new service file(s):
109
- - Add \`@provideSingleton()\` decorator
110
- - Move child methods to new class
111
-
112
- 4. **UPDATE** dependency injection:
113
- - Inject new service into original class constructor
114
- - Replace direct method calls with \`this.serviceName.method()\`
115
-
116
- 5. **VERIFY** file sizes:
117
- - Original file should now be under the limit
118
- - Each extracted file should be under the limit
119
-
120
- ## Escape Hatch
121
-
122
- If refactoring is genuinely not feasible, add a disable comment:
123
-
124
- \`\`\`typescript
125
- // eslint-disable-next-line @webpieces/max-file-lines
126
- \`\`\`
127
-
128
- Remember: Find the "child code" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.
129
- `;
130
27
  exports.default = maxFileLinesRule;
131
28
  //# sourceMappingURL=max-file-lines.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"max-file-lines.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/max-file-lines.ts"],"names":[],"mappings":";;;AAAA,+CAAyB;AACzB,mDAA6B;AAG7B,oCAA0C;AAE1C,MAAM,aAAa,GAAG,GAAG,CAAC;AAC1B,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAC9C,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAE9C,MAAM,gBAAgB,GAAa;IAC/B,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,6CAA6C;IAC1D,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE;IACxC,OAAO,EAAE;QACL,0FAA0F;QAC1F,0FAA0F;KAC7F;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,QAAQ;YAClD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAW;YAChC,CAAC,CAAC,aAAa,CAAC;QACpB,IAAI,GAAG,CAAC,kBAAkB,IAAI,KAAK;YAAE,OAAO,EAAE,CAAC;QAC/C,oBAAoB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACxC,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,cAAc,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,SAAS,EACrD,gBAAgB,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,yBAAyB,MAAM,CAAC,KAAK,CAAC,sGAAsG,CAC7L,CAAC,CAAC;IACP,CAAC;CACJ,CAAC;AAEF,SAAS,oBAAoB,CAAC,aAAqB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IAC/C,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO;IACpC,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AACrD,CAAC;AAED,qDAAqD;AACrD,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0F5B,CAAC;AAEF,kBAAe,gBAAgB,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport type { FileRule, FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst DEFAULT_LIMIT = 900;\nconst INSTRUCT_DIR = '.webpieces/instruct-ai';\nconst INSTRUCT_FILE = 'webpieces.filesize.md';\n\nconst maxFileLinesRule: FileRule = {\n name: 'max-file-lines',\n description: 'Cap file length at a configured line limit.',\n scope: 'file',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: { limit: DEFAULT_LIMIT },\n fixHint: [\n 'READ .webpieces/instruct-ai/webpieces.filesize.md for step-by-step refactoring guidance.',\n '// eslint-disable-next-line @webpieces/max-file-lines (also suppresses the eslint rule)',\n ],\n\n check(ctx: FileContext): readonly Violation[] {\n const limit = typeof ctx.options['limit'] === 'number'\n ? ctx.options['limit'] as number\n : DEFAULT_LIMIT;\n if (ctx.projectedFileLines <= limit) return [];\n writeInstructionFile(ctx.workspaceRoot);\n return [new V(\n 1,\n `(projected ${String(ctx.projectedFileLines)} lines)`,\n `File will be ${String(ctx.projectedFileLines)} lines, exceeding the ${String(limit)}-line limit. See .webpieces/instruct-ai/webpieces.filesize.md for detailed refactoring instructions.`,\n )];\n },\n};\n\nfunction writeInstructionFile(workspaceRoot: string): void {\n const dir = path.join(workspaceRoot, INSTRUCT_DIR);\n const filePath = path.join(dir, INSTRUCT_FILE);\n if (fs.existsSync(filePath)) return;\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(filePath, FILESIZE_DOC_CONTENT);\n}\n\n// eslint-disable-next-line @webpieces/max-file-lines\nconst FILESIZE_DOC_CONTENT = `# AI Agent Instructions: File Too Long\n\n**READ THIS FILE to fix files that are too long**\n\n## Core Principle\n\nWith **stateless systems + dependency injection, refactor is trivial**.\nPick a method or a few and move to new class XXXXX, then inject XXXXX\ninto all users of those methods via the constructor.\nDelete those methods from original class.\n\n**99% of files can be less than the configured max lines of code.**\n\nFiles should contain a SINGLE COHESIVE UNIT.\n- One class per file (Java convention)\n- If class is too large, extract child responsibilities\n- Use dependency injection to compose functionality\n\n## Command: Reduce File Size\n\n### Step 1: Check for Multiple Classes\nIf the file contains multiple classes, **SEPARATE each class into its own file**.\n\n### Step 2: Extract Child Responsibilities (if single class is too large)\n\n#### Pattern: Create New Service Class with Dependency Injection\n\n\\`\\`\\`typescript\n// BAD: UserController.ts (800 lines, single class)\n@provideSingleton()\n@Controller()\nexport class UserController {\n // 200 lines: CRUD operations\n // 300 lines: validation logic\n // 200 lines: notification logic\n}\n\n// GOOD: Extract validation service\n// 1. Create UserValidationService.ts\n@provideSingleton()\nexport class UserValidationService {\n validateUserData(data: UserData): ValidationResult { /* ... */ }\n validateEmail(email: string): boolean { /* ... */ }\n}\n\n// 2. Inject into UserController.ts\n@provideSingleton()\n@Controller()\nexport class UserController {\n constructor(\n @inject(TYPES.UserValidationService)\n private validator: UserValidationService\n ) {}\n}\n\\`\\`\\`\n\n## AI Agent Action Steps\n\n1. **ANALYZE** the file structure:\n - Count classes (if >1, separate immediately)\n - Identify logical responsibilities within single class\n\n2. **IDENTIFY** \"child code\" to extract:\n - Validation logic -> ValidationService\n - Notification logic -> NotificationService\n - Data transformation -> TransformerService\n - External API calls -> ApiService\n - Business rules -> RulesEngine\n\n3. **CREATE** new service file(s):\n - Add \\`@provideSingleton()\\` decorator\n - Move child methods to new class\n\n4. **UPDATE** dependency injection:\n - Inject new service into original class constructor\n - Replace direct method calls with \\`this.serviceName.method()\\`\n\n5. **VERIFY** file sizes:\n - Original file should now be under the limit\n - Each extracted file should be under the limit\n\n## Escape Hatch\n\nIf refactoring is genuinely not feasible, add a disable comment:\n\n\\`\\`\\`typescript\n// eslint-disable-next-line @webpieces/max-file-lines\n\\`\\`\\`\n\nRemember: Find the \"child code\" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.\n`;\n\nexport default maxFileLinesRule;\n"]}
1
+ {"version":3,"file":"max-file-lines.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/max-file-lines.ts"],"names":[],"mappings":";;AACA,oCAA0C;AAC1C,0DAAiE;AAEjE,MAAM,aAAa,GAAG,GAAG,CAAC;AAC1B,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAE9C,MAAM,gBAAgB,GAAa;IAC/B,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,6CAA6C;IAC1D,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE;IACxC,OAAO,EAAE;QACL,0FAA0F;QAC1F,0FAA0F;KAC7F;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,QAAQ;YAClD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAW;YAChC,CAAC,CAAC,aAAa,CAAC;QACpB,IAAI,GAAG,CAAC,kBAAkB,IAAI,KAAK;YAAE,OAAO,EAAE,CAAC;QAC/C,IAAA,qCAAsB,EAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QACzD,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,cAAc,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,SAAS,EACrD,gBAAgB,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,yBAAyB,MAAM,CAAC,KAAK,CAAC,sGAAsG,CAC7L,CAAC,CAAC;IACP,CAAC;CACJ,CAAC;AAEF,kBAAe,gBAAgB,CAAC","sourcesContent":["import type { FileRule, FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { writeTemplateIfMissing } from '@webpieces/rules-config';\n\nconst DEFAULT_LIMIT = 900;\nconst INSTRUCT_FILE = 'webpieces.filesize.md';\n\nconst maxFileLinesRule: FileRule = {\n name: 'max-file-lines',\n description: 'Cap file length at a configured line limit.',\n scope: 'file',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: { limit: DEFAULT_LIMIT },\n fixHint: [\n 'READ .webpieces/instruct-ai/webpieces.filesize.md for step-by-step refactoring guidance.',\n '// eslint-disable-next-line @webpieces/max-file-lines (also suppresses the eslint rule)',\n ],\n\n check(ctx: FileContext): readonly Violation[] {\n const limit = typeof ctx.options['limit'] === 'number'\n ? ctx.options['limit'] as number\n : DEFAULT_LIMIT;\n if (ctx.projectedFileLines <= limit) return [];\n writeTemplateIfMissing(ctx.workspaceRoot, INSTRUCT_FILE);\n return [new V(\n 1,\n `(projected ${String(ctx.projectedFileLines)} lines)`,\n `File will be ${String(ctx.projectedFileLines)} lines, exceeding the ${String(limit)}-line limit. See .webpieces/instruct-ai/webpieces.filesize.md for detailed refactoring instructions.`,\n )];\n },\n};\n\nexport default maxFileLinesRule;\n"]}
@@ -1,694 +0,0 @@
1
- # AI Agent Instructions: Try-Catch Blocks Detected
2
-
3
- **READ THIS FILE to understand why try-catch blocks are restricted and how to fix violations**
4
-
5
- ## GETTING STARTED: Rolling Out This Rule
6
-
7
- **Why this rule exists**: AI agents tend to randomly add try-catch blocks ~50% of the time, creating pointless error handling that swallows exceptions and breaks debugging.
8
-
9
- **How to roll out on existing codebases**:
10
- 1. Enable the rule: `'@webpieces/no-unmanaged-exceptions': 'error'`
11
- 2. Have AI add `// eslint-disable-next-line @webpieces/no-unmanaged-exceptions` to EACH try-catch line (NOT file-level disables)
12
- 3. This forces AI to consciously acknowledge each exception handling location
13
- 4. Going forward, the rule makes AI think twice before adding new try-catch blocks
14
-
15
- **What the global error handler provides** (when exceptions bubble up properly):
16
- 1. **Logs it** - Full error with stack trace and traceId
17
- 2. **Reports to operations** - Sends to monitoring (Sentry/Datadog) so AI/team can fix
18
- 3. **Shows user-friendly error** - Pops error dialog with errorId (user receives email with same ID for support)
19
-
20
- **Per-line disables are intentional**: Each disable comment serves as documentation explaining WHY that specific try-catch exists, making code review and future AI sessions aware of the exception handling decision.
21
-
22
- ## Core Principle
23
-
24
- **EXCEPTIONS MUST BUBBLE TO GLOBAL HANDLER WITH TRACEID FOR DEBUGGABILITY.**
25
-
26
- The webpieces framework uses a global error handling architecture where:
27
- - Every request gets a unique traceId stored in RequestContext
28
- - All errors bubble to the global handler (WebpiecesMiddleware.globalErrorHandler)
29
- - Error IDs enable lookup via `/debugLocal/{id}` and `/debugCloud/{id}` endpoints
30
- - Local try-catch blocks break this pattern by losing error IDs and context
31
-
32
- This is not a performance concern - it's an architecture decision for distributed tracing and debugging in production.
33
-
34
- ## Why This Rule Exists
35
-
36
- ### Problem 1: AI Over-Adds Try-Catch (Especially Frontend)
37
- AI agents tend to add defensive try-catch blocks everywhere, which:
38
- - Swallows errors and loses traceId
39
- - Shows custom error messages without debugging context
40
- - Makes production issues impossible to trace
41
- - Creates "blind spots" where errors disappear
42
-
43
- ### Problem 2: Lost TraceId = Lost Debugging Capability
44
- Without traceId in errors:
45
- - `/debugLocal/{id}` endpoint cannot retrieve error details
46
- - `/debugCloud/{id}` endpoint cannot correlate logs
47
- - DevOps cannot trace request flow through distributed systems
48
- - Users report "an error occurred" with no way to investigate
49
-
50
- ### Problem 3: Pointless Try-Catch-Rethrow
51
- ```typescript
52
- // BAD: Catching just to rethrow without adding value
53
- try {
54
- await operation();
55
- } catch (err: unknown) {
56
- const error = toError(err);
57
- console.error('Failed:', error);
58
- throw error; // No new info added - why catch?
59
- }
60
- ```
61
-
62
- **However, try-catch-rethrow IS acceptable when:**
63
- 1. **Adding context to the error**: `throw new Error("Failed to process order #123", { cause: error })`
64
- 2. **Edge code logging** (see "Edge Code Patterns" section below)
65
-
66
- The key question: Are you adding meaningful information or context? If yes, it may be valid.
67
-
68
- ### Problem 4: Swallowing Exceptions = Lazy Programming
69
- ```typescript
70
- // BAD: "I don't want to deal with this error"
71
- try {
72
- await riskyOperation();
73
- } catch (err: unknown) {
74
- // Silence...
75
- }
76
- ```
77
- This is the #1 shortcut developers take that creates production nightmares.
78
-
79
- ## Industry Best Practices (2025)
80
-
81
- ### Distributed Tracing: The Three Pillars
82
- Modern observability requires correlation across:
83
- 1. **Traces** - Request flow through services
84
- 2. **Logs** - Contextual debugging information
85
- 3. **Metrics** - Aggregated system health
86
-
87
- TraceId (also called correlation ID, request ID) ties these together.
88
-
89
- ### Research Findings
90
- - **Performance**: Try-catch is an expensive operation in V8 engine (source: Node.js performance docs)
91
- - **Error Handling**: Global handlers at highest level reduce blind spots by 40% (source: Google SRE practices)
92
- - **Middleware Pattern**: Express/Koa middleware with async error boundaries is industry standard (source: Express.js error handling docs)
93
- - **Only Catch What You Can Handle**: If you can't recover, let it bubble (source: "Effective Error Handling" - JavaScript design patterns)
94
-
95
- ### 2025 Trends
96
- - Correlation IDs are standard in microservices (OpenTelemetry, Datadog, New Relic)
97
- - Structured logging with context (Winston, Pino)
98
- - Middleware-based error boundaries reduce boilerplate
99
- - Frontend: React Error Boundaries, not scattered try-catch
100
-
101
- ## Command: Remove Try-Catch and Use Global Handler
102
-
103
- ## AI Agent Action Steps
104
-
105
- 1. **IDENTIFY** the try-catch block flagged in the error message
106
-
107
- 2. **ANALYZE** the purpose and ASK USER if needed:
108
- - Is it catching errors just to log them? → Remove (use LogApiFilter)
109
- - Is it catching to show custom message? → Remove (use global handler)
110
- - Is it catching to retry? → Requires approval (see Acceptable Patterns)
111
- - Is it catching in a batch loop? → Requires approval (see Acceptable Patterns)
112
- - Is it catching for cleanup? → Usually wrong pattern
113
- - **Is this a global entry point?** → **ASK USER**: "I think this code is the entry point where we need a global try-catch block. Is this correct?" (95% of the time it is NOT!)
114
- - **Is this edge code calling external services?** → **ASK USER**: "This looks like edge code calling an external service. Should I add request/response logging with try-catch?"
115
- - **Is this form error handling?** → Valid IF: catches only `HttpUserError` for display AND rethrows other errors (see Form Error Handling Pattern)
116
- - Is it adding context to the error before rethrowing? → May be valid (see Problem 3)
117
-
118
- 3. **IF REMOVING** the try-catch block:
119
- - Delete the `try {` and `} catch (err: unknown) { ... }` wrapper
120
- - Let the code execute normally
121
- - Errors will bubble to global handler automatically
122
-
123
- 4. **IF KEEPING** (after user approval):
124
- - Add eslint-disable comment with justification
125
- - Ensure traceId is logged/preserved
126
- - Follow patterns in "Global Try-Catch Entry Points" or "Edge Code Patterns" sections
127
-
128
- 5. **VERIFY** global handler exists:
129
- - Check that WebpiecesMiddleware.globalErrorHandler is registered
130
- - Check that ContextFilter is setting up RequestContext
131
- - Check that traceId is being added to RequestContext
132
-
133
- 6. **ADD** traceId to RequestContext (if not already present):
134
- - In ContextFilter or similar high-priority filter
135
- - Use `RequestContext.put('TRACE_ID', generateTraceId())`
136
-
137
- 7. **TEST** error flow:
138
- - Trigger an error in the code
139
- - Verify error is logged with traceId
140
- - Verify `/debugLocal/{traceId}` endpoint works
141
-
142
- ## Pattern 1: Global Error Handler (GOOD)
143
-
144
- ### Server-Side: WebpiecesMiddleware
145
-
146
- ```typescript
147
- // packages/http/http-server/src/WebpiecesMiddleware.ts
148
- @provideSingleton()
149
- @injectable()
150
- export class WebpiecesMiddleware {
151
- async globalErrorHandler(
152
- req: Request,
153
- res: Response,
154
- next: NextFunction
155
- ): Promise<void> {
156
- console.log('[GlobalErrorHandler] Request START:', req.method, req.path);
157
-
158
- try {
159
- // Await catches BOTH sync throws AND rejected promises
160
- await next();
161
- console.log('[GlobalErrorHandler] Request END (success)');
162
- } catch (err: unknown) {
163
- const error = toError(err);
164
- const traceId = RequestContext.get<string>('TRACE_ID');
165
-
166
- // Log with traceId for /debugLocal lookup
167
- console.error('[GlobalErrorHandler] ERROR:', {
168
- traceId,
169
- message: error.message,
170
- stack: error.stack,
171
- path: req.path,
172
- method: req.method,
173
- });
174
-
175
- // Store error for /debugLocal/{id} endpoint
176
- ErrorStore.save(traceId, error);
177
-
178
- if (!res.headersSent) {
179
- res.status(500).send(`
180
- <!DOCTYPE html>
181
- <html>
182
- <head><title>Server Error</title></head>
183
- <body>
184
- <h1>Server Error</h1>
185
- <p>An error occurred. Reference ID: ${traceId}</p>
186
- <p>Contact support with this ID to investigate.</p>
187
- </body>
188
- </html>
189
- `);
190
- }
191
- }
192
- }
193
- }
194
- ```
195
-
196
- ### Adding TraceId: ContextFilter
197
-
198
- ```typescript
199
- // packages/http/http-server/src/filters/ContextFilter.ts
200
- import { v4 as uuidv4 } from 'uuid';
201
-
202
- @provideSingleton()
203
- @injectable()
204
- export class ContextFilter extends Filter<MethodMeta, WpResponse<unknown>> {
205
- async filter(
206
- meta: MethodMeta,
207
- nextFilter: Service<MethodMeta, WpResponse<unknown>>
208
- ): Promise<WpResponse<unknown>> {
209
- return RequestContext.run(async () => {
210
- // Generate unique traceId for this request
211
- const traceId = uuidv4();
212
- RequestContext.put('TRACE_ID', traceId);
213
- RequestContext.put('METHOD_META', meta);
214
- RequestContext.put('REQUEST_PATH', meta.path);
215
-
216
- return await nextFilter.invoke(meta);
217
- // RequestContext auto-cleared when done
218
- });
219
- }
220
- }
221
- ```
222
-
223
- ## Pattern 2: Debug Endpoints (GOOD)
224
-
225
- ```typescript
226
- // Example debug endpoint for local development
227
- @provideSingleton()
228
- @Controller()
229
- export class DebugController implements DebugApi {
230
- @Get()
231
- @Path('/debugLocal/:id')
232
- async getErrorById(@PathParam('id') id: string): Promise<DebugErrorResponse> {
233
- const error = ErrorStore.get(id);
234
- if (!error) {
235
- throw new HttpNotFoundError(`Error ${id} not found`);
236
- }
237
-
238
- return {
239
- traceId: id,
240
- message: error.message,
241
- stack: error.stack,
242
- timestamp: error.timestamp,
243
- requestPath: error.requestPath,
244
- requestMethod: error.requestMethod,
245
- };
246
- }
247
- }
248
-
249
- // ErrorStore singleton (in-memory for local, Redis for production)
250
- class ErrorStoreImpl {
251
- private errors = new Map<string, ErrorRecord>();
252
-
253
- save(traceId: string, error: Error): void {
254
- this.errors.set(traceId, {
255
- traceId,
256
- message: error.message,
257
- stack: error.stack,
258
- timestamp: new Date(),
259
- requestPath: RequestContext.get('REQUEST_PATH'),
260
- requestMethod: RequestContext.get('HTTP_METHOD'),
261
- });
262
- }
263
-
264
- get(traceId: string): ErrorRecord | undefined {
265
- return this.errors.get(traceId);
266
- }
267
- }
268
-
269
- export const ErrorStore = new ErrorStoreImpl();
270
- ```
271
-
272
- ## Examples
273
-
274
- ### BAD Example 1: Local Try-Catch That Swallows Error
275
-
276
- ```typescript
277
- // BAD: Error is swallowed, no traceId in logs
278
- async function processOrder(order: Order): Promise<void> {
279
- try {
280
- await validateOrder(order);
281
- await saveToDatabase(order);
282
- } catch (err: unknown) {
283
- // Error disappears into void - debugging nightmare!
284
- console.log('Order processing failed');
285
- }
286
- }
287
- ```
288
-
289
- **Problem**: When this fails in production, you have:
290
- - No traceId to look up the error
291
- - No stack trace
292
- - No request context
293
- - No way to investigate
294
-
295
- ### BAD Example 2: Try-Catch With Custom Error (No TraceId)
296
-
297
- ```typescript
298
- // BAD: Shows custom message but loses traceId
299
- async function fetchUserData(userId: string): Promise<User> {
300
- try {
301
- const response = await fetch(`/api/users/${userId}`);
302
- return await response.json();
303
- } catch (err: unknown) {
304
- const error = toError(err);
305
- // Custom message without traceId
306
- throw new Error(`Failed to fetch user ${userId}: ${error.message}`);
307
- }
308
- }
309
- ```
310
-
311
- **Problem**:
312
- - Original error context is lost
313
- - No traceId attached to new error
314
- - Global handler receives generic error, can't trace root cause
315
-
316
- ### GOOD Example 1: Let Error Bubble
317
-
318
- ```typescript
319
- // GOOD: Error bubbles to global handler with traceId
320
- async function processOrder(order: Order): Promise<void> {
321
- // No try-catch needed!
322
- await validateOrder(order);
323
- await saveToDatabase(order);
324
- // If error occurs, it bubbles with traceId intact
325
- }
326
- ```
327
-
328
- **Why GOOD**:
329
- - Global handler catches error
330
- - TraceId from RequestContext is preserved
331
- - Full stack trace available
332
- - `/debugLocal/{traceId}` endpoint works
333
-
334
- ### GOOD Example 2: Global Handler Logs With TraceId
335
-
336
- ```typescript
337
- // GOOD: Global handler has full context
338
- // In WebpiecesMiddleware.globalErrorHandler (see Pattern 1 above)
339
- catch (err: unknown) {
340
- const error = toError(err);
341
- const traceId = RequestContext.get<string>('TRACE_ID');
342
-
343
- console.error('[GlobalErrorHandler] ERROR:', {
344
- traceId, // Unique ID for this request
345
- message: error.message,
346
- stack: error.stack,
347
- path: req.path, // Request context preserved
348
- });
349
- }
350
- ```
351
-
352
- **Why GOOD**:
353
- - TraceId logged with every error
354
- - Full request context available
355
- - Error stored for `/debugLocal/{id}` lookup
356
- - DevOps can trace distributed requests
357
-
358
- ### ACCEPTABLE Example 1: Retry Loop (With eslint-disable)
359
-
360
- ```typescript
361
- // ACCEPTABLE: Retry pattern requires try-catch
362
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Retry loop with exponential backoff
363
- async function callVendorApiWithRetry(request: VendorRequest): Promise<VendorResponse> {
364
- const maxRetries = 3;
365
- let lastError: Error | undefined;
366
-
367
- for (let i = 0; i < maxRetries; i++) {
368
- try {
369
- return await vendorApi.call(request);
370
- } catch (err: unknown) {
371
- const error = toError(err);
372
- lastError = error;
373
- console.warn(`Retry ${i + 1}/${maxRetries} failed:`, error.message);
374
- await sleep(1000 * Math.pow(2, i)); // Exponential backoff
375
- }
376
- }
377
-
378
- // After retries exhausted, throw with traceId
379
- const traceId = RequestContext.get<string>('TRACE_ID');
380
- throw new HttpVendorError(
381
- `Vendor API failed after ${maxRetries} retries. TraceId: ${traceId}`,
382
- lastError
383
- );
384
- }
385
- ```
386
-
387
- **Why ACCEPTABLE**:
388
- - Legitimate use case: retry logic
389
- - Final error still includes traceId
390
- - Error still bubbles to global handler
391
- - Requires senior developer approval (enforced by PR review)
392
-
393
- ### ACCEPTABLE Example 2: Batching Pattern (With eslint-disable)
394
-
395
- ```typescript
396
- // ACCEPTABLE: Batching requires try-catch to continue processing
397
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Batch processing continues on individual failures
398
- async function processBatch(items: Item[]): Promise<BatchResult> {
399
- const results: ItemResult[] = [];
400
- const errors: ItemError[] = [];
401
- const traceId = RequestContext.get<string>('TRACE_ID');
402
-
403
- for (const item of items) {
404
- try {
405
- const result = await processItem(item);
406
- results.push(result);
407
- } catch (err: unknown) {
408
- const error = toError(err);
409
- // Log individual error with traceId
410
- console.error(`[Batch] Item ${item.id} failed (traceId: ${traceId}):`, error);
411
- errors.push({ itemId: item.id, error: error.message, traceId });
412
- }
413
- }
414
-
415
- // Return both successes and failures
416
- return {
417
- traceId,
418
- successCount: results.length,
419
- failureCount: errors.length,
420
- results,
421
- errors,
422
- };
423
- }
424
- ```
425
-
426
- **Why ACCEPTABLE**:
427
- - Legitimate use case: partial failure handling
428
- - Each error logged with traceId
429
- - Batch traceId included in response
430
- - Requires senior developer approval (enforced by PR review)
431
-
432
- ### UNACCEPTABLE Example: Pointless Try-Catch-Rethrow (Internal Code)
433
-
434
- ```typescript
435
- // UNACCEPTABLE: Pointless try-catch in INTERNAL code
436
- async function saveUser(user: User): Promise<void> {
437
- try {
438
- await userRepository.save(user); // Internal call, not edge
439
- } catch (err: unknown) {
440
- const error = toError(err);
441
- console.error('Save failed:', error);
442
- throw error; // No value added - why catch?
443
- }
444
- }
445
- ```
446
-
447
- **Why UNACCEPTABLE for internal code**:
448
- - Adds no value - logging should be in LogApiFilter
449
- - Global handler already logs errors
450
- - Just adds noise and confusion
451
- - Remove the try-catch entirely!
452
-
453
- **CONTRAST with edge code (ACCEPTABLE)**:
454
- ```typescript
455
- // ACCEPTABLE: Edge code calling external database service
456
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Edge code: database logging
457
- async function saveUserToDb(user: User): Promise<void> {
458
- const traceId = RequestContext.get<string>('TRACE_ID');
459
- try {
460
- logRequest('[DB] Saving user', { traceId, userId: user.id });
461
- await externalDbClient.save('users', user); // EDGE: external service
462
- logSuccess('[DB] User saved', { traceId, userId: user.id });
463
- } catch (err: unknown) {
464
- const error = toError(err);
465
- logFailure('[DB] Save failed', { traceId, userId: user.id, error: error.message });
466
- throw error; // Rethrow - logging value at the edge
467
- }
468
- }
469
- ```
470
-
471
- **The difference**: Edge code benefits from request/response/failure logging at the service boundary. Internal code does not.
472
-
473
- ## When eslint-disable IS Acceptable
474
-
475
- You may use `// eslint-disable-next-line @webpieces/no-unmanaged-exceptions` ONLY for:
476
-
477
- 1. **Retry loops** with exponential backoff (vendor API calls)
478
- 2. **Batching patterns** where partial failure is expected
479
- 3. **Resource cleanup** with explicit approval
480
- 4. **Global error handler entry points** (see below)
481
- 5. **Edge code patterns** for vendor/external service calls (see below)
482
- 6. **Form error handling** - catching `HttpUserError` for display, rethrowing others (see below)
483
-
484
- All require:
485
- - Comment explaining WHY try-catch is needed
486
- - TraceId must still be logged/included in final error (or error must be rethrown)
487
-
488
- ## Global Try-Catch Entry Points (MUST ASK USER)
489
-
490
- **CRITICAL: 95% of the time, the code you're looking at is NOT a global entry point!**
491
-
492
- Before adding a global try-catch, **AI agents MUST ask the user**: "I think this code is the entry point where we need a global try-catch block. Is this correct?"
493
-
494
- ### Examples of LEGITIMATE Global Error Handlers
495
-
496
- These are the rare places where global try-catch IS correct:
497
-
498
- 1. **Node.js/Express middleware** (at the TOP, after setting up traceId in context):
499
- ```typescript
500
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Global error handler entry point
501
- app.use(async (req, res, next) => {
502
- // First: set up traceId in RequestContext
503
- const traceId = uuidv4();
504
- RequestContext.put('TRACE_ID', traceId);
505
-
506
- try {
507
- await next();
508
- } catch (err: unknown) {
509
- const error = toError(err);
510
- // Report to Sentry/observability
511
- Sentry.captureException(error, { extra: { traceId } });
512
- res.status(500).json({ error: 'Internal error', traceId });
513
- }
514
- });
515
- ```
516
-
517
- 2. **RxJS global error handler**:
518
- ```typescript
519
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- RxJS global unhandled error hook
520
- config.onUnhandledError = (err: any) => {
521
- const error = toError(err);
522
- Sentry.captureException(error);
523
- console.error('[RxJS Unhandled]', error);
524
- };
525
- ```
526
-
527
- 3. **Browser window unhandled promise rejection**:
528
- ```typescript
529
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Browser global unhandled promise handler
530
- window.addEventListener('unhandledrejection', (event) => {
531
- const error = toError(event.reason);
532
- Sentry.captureException(error);
533
- console.error('[Unhandled Promise]', error);
534
- });
535
- ```
536
-
537
- 4. **Angular ErrorHandler** (may need try-catch to prevent double recording):
538
- ```typescript
539
- @Injectable()
540
- export class GlobalErrorHandler implements ErrorHandler {
541
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Angular global error handler
542
- handleError(error: any): void {
543
- try {
544
- Sentry.captureException(error);
545
- } catch (sentryError) {
546
- // Prevent infinite loop if Sentry itself fails
547
- console.error('[Sentry failed]', sentryError);
548
- }
549
- console.error('[Angular Error]', error);
550
- }
551
- }
552
- ```
553
-
554
- 5. **3rd party vendor event listeners**:
555
- ```typescript
556
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Vendor callback error boundary
557
- vendorSdk.on('event', async (data) => {
558
- try {
559
- await processVendorEvent(data);
560
- } catch (err: unknown) {
561
- const error = toError(err);
562
- const traceId = RequestContext.get<string>('TRACE_ID');
563
- Sentry.captureException(error, { extra: { traceId, vendorData: data } });
564
- // Don't rethrow - vendor SDK may not handle errors gracefully
565
- }
566
- });
567
- ```
568
-
569
- **All global handlers should report to observability (Sentry, Datadog, etc.) in production.**
570
-
571
- ## Edge Code Patterns (MUST ASK USER)
572
-
573
- Edge code is code that interacts with external systems (vendors, APIs, databases, email services, etc.). These often benefit from a try-catch pattern for **logging the full request/response cycle**.
574
-
575
- **AI agents MUST ask the user** before adding edge code try-catch: "This looks like edge code calling an external service. Should I add request/response logging with try-catch?"
576
-
577
- ### Example: sendMail Pattern
578
- ```typescript
579
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Edge code: external email service logging
580
- async function sendMail(request: MailRequest): Promise<MailResponse> {
581
- const traceId = RequestContext.get<string>('TRACE_ID');
582
-
583
- try {
584
- logRequest('[Email] Sending', { traceId, to: request.to, subject: request.subject });
585
- const response = await emailService.send(request);
586
- logSuccess('[Email] Sent', { traceId, messageId: response.messageId });
587
- return response;
588
- } catch (err: unknown) {
589
- const error = toError(err);
590
- logFailure('[Email] Failed', { traceId, error: error.message, to: request.to });
591
- throw error; // Rethrow - adds logging value at the edge
592
- }
593
- }
594
- ```
595
-
596
- ### Why This Pattern Is Valuable at Edges
597
-
598
- 1. **Complete audit trail**: Request logged, then either success OR failure logged
599
- 2. **Vendor debugging**: When vendor says "we never received it", you have proof
600
- 3. **Performance monitoring**: Track timing at service boundaries
601
- 4. **Correlation**: TraceId connects this edge call to the overall request
602
-
603
- ### Where Edge Code Patterns Apply
604
-
605
- - HTTP client calls to external APIs
606
- - Database operations (especially writes)
607
- - Message queue publish/consume
608
- - Email/SMS/notification services
609
- - Payment gateway calls
610
- - File storage operations (S3, GCS, etc.)
611
- - Any call leaving your service boundary
612
-
613
- ## Form Error Handling Pattern (Client-Side)
614
-
615
- Frontend forms often need to catch user-facing errors (like validation errors) to display in the UI, while rethrowing unexpected errors to the global handler.
616
-
617
- **This pattern is ACCEPTABLE because**:
618
- - It catches ONLY user-facing errors (`HttpUserError`) for display
619
- - Unexpected errors are RETHROWN (not swallowed)
620
- - Server throws `HttpUserError` → protocol translates to error payload → client translates back to exception
621
-
622
- ### Example: Form Submission Error Handling
623
- ```typescript
624
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Form error display: catch user errors, rethrow others
625
- async submitForm(): Promise<void> {
626
- try {
627
- await this.apiClient.saveData(this.formData);
628
- this.router.navigate(['/success']);
629
- } catch (err: unknown) {
630
- const error = toError(err);
631
-
632
- if (error instanceof HttpUserError) {
633
- // User-facing error - display in form
634
- this.formError = error.message;
635
- this.cdr.detectChanges();
636
- } else {
637
- // Unexpected error - let global handler deal with it
638
- throw error;
639
- }
640
- }
641
- }
642
- ```
643
-
644
- ### Why This Pattern Is Valid
645
-
646
- 1. **Selective catching**: Only catches errors meant for user display
647
- 2. **No swallowing**: Unexpected errors bubble to global handler with traceId
648
- 3. **Protocol design**: Server intentionally throws `HttpUserError` for user-facing messages
649
- 4. **UX requirement**: Forms must show validation errors inline, not via global error page
650
-
651
- ### Key Requirements
652
-
653
- - **ONLY catch specific error types** (e.g., `HttpUserError`, `ValidationError`)
654
- - **ALWAYS rethrow** errors that aren't user-facing
655
- - The server-side code should throw `HttpUserError` for user-displayable messages
656
-
657
- ## How to Request Approval
658
-
659
- If you believe you have a legitimate use case for try-catch:
660
-
661
- 1. **Add a comment** explaining why it's needed:
662
- ```typescript
663
- // JUSTIFICATION: Vendor API requires retry loop with exponential backoff
664
- // to handle rate limiting. Final error includes traceId for debugging.
665
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
666
- ```
667
-
668
- 2. **Ensure traceId is preserved** in final error or logged
669
-
670
- 3. **Request PR review** from senior developer
671
-
672
- 4. **Be prepared to justify** - 99% of try-catch can be removed
673
-
674
- ## Summary
675
-
676
- **The webpieces philosophy**: Errors should bubble to the global handler where they are logged with traceId and stored for debugging. Local try-catch blocks break this architecture and create blind spots in production.
677
-
678
- **Key takeaways**:
679
- - Global error handler with traceId = debuggable production issues
680
- - Local try-catch in internal code = lost context and debugging nightmares
681
- - 95% of try-catch blocks can be removed safely
682
- - Acceptable try-catch uses: retries, batching, global entry points, edge code
683
- - **AI agents MUST ask user** before adding global try-catch or edge code patterns
684
- - TraceId enables `/debugLocal/{id}` and `/debugCloud/{id}` endpoints
685
-
686
- **Acceptable patterns (with eslint-disable)**:
687
- 1. **Global entry points**: Express middleware, RxJS error hooks, Angular ErrorHandler, browser unhandledrejection
688
- 2. **Edge code**: External API calls, database operations, email services - use logRequest/logSuccess/logFailure pattern
689
- 3. **Retry loops**: Vendor APIs with exponential backoff
690
- 4. **Batching**: Partial failure handling where processing must continue
691
- 5. **Form error handling**: Catch `HttpUserError` for UI display, rethrow all other errors
692
-
693
- **Remember**: If you can't handle the error meaningfully, don't catch it. Let it bubble to the global handler where it will be logged with full context and traceId.
694
-