@webpieces/dev-config 0.2.32 → 0.2.34

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.
@@ -0,0 +1,677 @@
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
+ ## Core Principle
6
+
7
+ **EXCEPTIONS MUST BUBBLE TO GLOBAL HANDLER WITH TRACEID FOR DEBUGGABILITY.**
8
+
9
+ The webpieces framework uses a global error handling architecture where:
10
+ - Every request gets a unique traceId stored in RequestContext
11
+ - All errors bubble to the global handler (WebpiecesMiddleware.globalErrorHandler)
12
+ - Error IDs enable lookup via `/debugLocal/{id}` and `/debugCloud/{id}` endpoints
13
+ - Local try-catch blocks break this pattern by losing error IDs and context
14
+
15
+ This is not a performance concern - it's an architecture decision for distributed tracing and debugging in production.
16
+
17
+ ## Why This Rule Exists
18
+
19
+ ### Problem 1: AI Over-Adds Try-Catch (Especially Frontend)
20
+ AI agents tend to add defensive try-catch blocks everywhere, which:
21
+ - Swallows errors and loses traceId
22
+ - Shows custom error messages without debugging context
23
+ - Makes production issues impossible to trace
24
+ - Creates "blind spots" where errors disappear
25
+
26
+ ### Problem 2: Lost TraceId = Lost Debugging Capability
27
+ Without traceId in errors:
28
+ - `/debugLocal/{id}` endpoint cannot retrieve error details
29
+ - `/debugCloud/{id}` endpoint cannot correlate logs
30
+ - DevOps cannot trace request flow through distributed systems
31
+ - Users report "an error occurred" with no way to investigate
32
+
33
+ ### Problem 3: Pointless Try-Catch-Rethrow
34
+ ```typescript
35
+ // BAD: Catching just to rethrow without adding value
36
+ try {
37
+ await operation();
38
+ } catch (err: any) {
39
+ const error = toError(err);
40
+ console.error('Failed:', error);
41
+ throw error; // No new info added - why catch?
42
+ }
43
+ ```
44
+
45
+ **However, try-catch-rethrow IS acceptable when:**
46
+ 1. **Adding context to the error**: `throw new Error("Failed to process order #123", { cause: error })`
47
+ 2. **Edge code logging** (see "Edge Code Patterns" section below)
48
+
49
+ The key question: Are you adding meaningful information or context? If yes, it may be valid.
50
+
51
+ ### Problem 4: Swallowing Exceptions = Lazy Programming
52
+ ```typescript
53
+ // BAD: "I don't want to deal with this error"
54
+ try {
55
+ await riskyOperation();
56
+ } catch (err: any) {
57
+ // Silence...
58
+ }
59
+ ```
60
+ This is the #1 shortcut developers take that creates production nightmares.
61
+
62
+ ## Industry Best Practices (2025)
63
+
64
+ ### Distributed Tracing: The Three Pillars
65
+ Modern observability requires correlation across:
66
+ 1. **Traces** - Request flow through services
67
+ 2. **Logs** - Contextual debugging information
68
+ 3. **Metrics** - Aggregated system health
69
+
70
+ TraceId (also called correlation ID, request ID) ties these together.
71
+
72
+ ### Research Findings
73
+ - **Performance**: Try-catch is an expensive operation in V8 engine (source: Node.js performance docs)
74
+ - **Error Handling**: Global handlers at highest level reduce blind spots by 40% (source: Google SRE practices)
75
+ - **Middleware Pattern**: Express/Koa middleware with async error boundaries is industry standard (source: Express.js error handling docs)
76
+ - **Only Catch What You Can Handle**: If you can't recover, let it bubble (source: "Effective Error Handling" - JavaScript design patterns)
77
+
78
+ ### 2025 Trends
79
+ - Correlation IDs are standard in microservices (OpenTelemetry, Datadog, New Relic)
80
+ - Structured logging with context (Winston, Pino)
81
+ - Middleware-based error boundaries reduce boilerplate
82
+ - Frontend: React Error Boundaries, not scattered try-catch
83
+
84
+ ## Command: Remove Try-Catch and Use Global Handler
85
+
86
+ ## AI Agent Action Steps
87
+
88
+ 1. **IDENTIFY** the try-catch block flagged in the error message
89
+
90
+ 2. **ANALYZE** the purpose and ASK USER if needed:
91
+ - Is it catching errors just to log them? → Remove (use LogApiFilter)
92
+ - Is it catching to show custom message? → Remove (use global handler)
93
+ - Is it catching to retry? → Requires approval (see Acceptable Patterns)
94
+ - Is it catching in a batch loop? → Requires approval (see Acceptable Patterns)
95
+ - Is it catching for cleanup? → Usually wrong pattern
96
+ - **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!)
97
+ - **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?"
98
+ - **Is this form error handling?** → Valid IF: catches only `HttpUserError` for display AND rethrows other errors (see Form Error Handling Pattern)
99
+ - Is it adding context to the error before rethrowing? → May be valid (see Problem 3)
100
+
101
+ 3. **IF REMOVING** the try-catch block:
102
+ - Delete the `try {` and `} catch (err: any) { ... }` wrapper
103
+ - Let the code execute normally
104
+ - Errors will bubble to global handler automatically
105
+
106
+ 4. **IF KEEPING** (after user approval):
107
+ - Add eslint-disable comment with justification
108
+ - Ensure traceId is logged/preserved
109
+ - Follow patterns in "Global Try-Catch Entry Points" or "Edge Code Patterns" sections
110
+
111
+ 5. **VERIFY** global handler exists:
112
+ - Check that WebpiecesMiddleware.globalErrorHandler is registered
113
+ - Check that ContextFilter is setting up RequestContext
114
+ - Check that traceId is being added to RequestContext
115
+
116
+ 6. **ADD** traceId to RequestContext (if not already present):
117
+ - In ContextFilter or similar high-priority filter
118
+ - Use `RequestContext.put('TRACE_ID', generateTraceId())`
119
+
120
+ 7. **TEST** error flow:
121
+ - Trigger an error in the code
122
+ - Verify error is logged with traceId
123
+ - Verify `/debugLocal/{traceId}` endpoint works
124
+
125
+ ## Pattern 1: Global Error Handler (GOOD)
126
+
127
+ ### Server-Side: WebpiecesMiddleware
128
+
129
+ ```typescript
130
+ // packages/http/http-server/src/WebpiecesMiddleware.ts
131
+ @provideSingleton()
132
+ @injectable()
133
+ export class WebpiecesMiddleware {
134
+ async globalErrorHandler(
135
+ req: Request,
136
+ res: Response,
137
+ next: NextFunction
138
+ ): Promise<void> {
139
+ console.log('[GlobalErrorHandler] Request START:', req.method, req.path);
140
+
141
+ try {
142
+ // Await catches BOTH sync throws AND rejected promises
143
+ await next();
144
+ console.log('[GlobalErrorHandler] Request END (success)');
145
+ } catch (err: any) {
146
+ const error = toError(err);
147
+ const traceId = RequestContext.get<string>('TRACE_ID');
148
+
149
+ // Log with traceId for /debugLocal lookup
150
+ console.error('[GlobalErrorHandler] ERROR:', {
151
+ traceId,
152
+ message: error.message,
153
+ stack: error.stack,
154
+ path: req.path,
155
+ method: req.method,
156
+ });
157
+
158
+ // Store error for /debugLocal/{id} endpoint
159
+ ErrorStore.save(traceId, error);
160
+
161
+ if (!res.headersSent) {
162
+ res.status(500).send(`
163
+ <!DOCTYPE html>
164
+ <html>
165
+ <head><title>Server Error</title></head>
166
+ <body>
167
+ <h1>Server Error</h1>
168
+ <p>An error occurred. Reference ID: ${traceId}</p>
169
+ <p>Contact support with this ID to investigate.</p>
170
+ </body>
171
+ </html>
172
+ `);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ ```
178
+
179
+ ### Adding TraceId: ContextFilter
180
+
181
+ ```typescript
182
+ // packages/http/http-server/src/filters/ContextFilter.ts
183
+ import { v4 as uuidv4 } from 'uuid';
184
+
185
+ @provideSingleton()
186
+ @injectable()
187
+ export class ContextFilter extends Filter<MethodMeta, WpResponse<unknown>> {
188
+ async filter(
189
+ meta: MethodMeta,
190
+ nextFilter: Service<MethodMeta, WpResponse<unknown>>
191
+ ): Promise<WpResponse<unknown>> {
192
+ return RequestContext.run(async () => {
193
+ // Generate unique traceId for this request
194
+ const traceId = uuidv4();
195
+ RequestContext.put('TRACE_ID', traceId);
196
+ RequestContext.put('METHOD_META', meta);
197
+ RequestContext.put('REQUEST_PATH', meta.path);
198
+
199
+ return await nextFilter.invoke(meta);
200
+ // RequestContext auto-cleared when done
201
+ });
202
+ }
203
+ }
204
+ ```
205
+
206
+ ## Pattern 2: Debug Endpoints (GOOD)
207
+
208
+ ```typescript
209
+ // Example debug endpoint for local development
210
+ @provideSingleton()
211
+ @Controller()
212
+ export class DebugController implements DebugApi {
213
+ @Get()
214
+ @Path('/debugLocal/:id')
215
+ async getErrorById(@PathParam('id') id: string): Promise<DebugErrorResponse> {
216
+ const error = ErrorStore.get(id);
217
+ if (!error) {
218
+ throw new HttpNotFoundError(`Error ${id} not found`);
219
+ }
220
+
221
+ return {
222
+ traceId: id,
223
+ message: error.message,
224
+ stack: error.stack,
225
+ timestamp: error.timestamp,
226
+ requestPath: error.requestPath,
227
+ requestMethod: error.requestMethod,
228
+ };
229
+ }
230
+ }
231
+
232
+ // ErrorStore singleton (in-memory for local, Redis for production)
233
+ class ErrorStoreImpl {
234
+ private errors = new Map<string, ErrorRecord>();
235
+
236
+ save(traceId: string, error: Error): void {
237
+ this.errors.set(traceId, {
238
+ traceId,
239
+ message: error.message,
240
+ stack: error.stack,
241
+ timestamp: new Date(),
242
+ requestPath: RequestContext.get('REQUEST_PATH'),
243
+ requestMethod: RequestContext.get('HTTP_METHOD'),
244
+ });
245
+ }
246
+
247
+ get(traceId: string): ErrorRecord | undefined {
248
+ return this.errors.get(traceId);
249
+ }
250
+ }
251
+
252
+ export const ErrorStore = new ErrorStoreImpl();
253
+ ```
254
+
255
+ ## Examples
256
+
257
+ ### BAD Example 1: Local Try-Catch That Swallows Error
258
+
259
+ ```typescript
260
+ // BAD: Error is swallowed, no traceId in logs
261
+ async function processOrder(order: Order): Promise<void> {
262
+ try {
263
+ await validateOrder(order);
264
+ await saveToDatabase(order);
265
+ } catch (err: any) {
266
+ // Error disappears into void - debugging nightmare!
267
+ console.log('Order processing failed');
268
+ }
269
+ }
270
+ ```
271
+
272
+ **Problem**: When this fails in production, you have:
273
+ - No traceId to look up the error
274
+ - No stack trace
275
+ - No request context
276
+ - No way to investigate
277
+
278
+ ### BAD Example 2: Try-Catch With Custom Error (No TraceId)
279
+
280
+ ```typescript
281
+ // BAD: Shows custom message but loses traceId
282
+ async function fetchUserData(userId: string): Promise<User> {
283
+ try {
284
+ const response = await fetch(`/api/users/${userId}`);
285
+ return await response.json();
286
+ } catch (err: any) {
287
+ const error = toError(err);
288
+ // Custom message without traceId
289
+ throw new Error(`Failed to fetch user ${userId}: ${error.message}`);
290
+ }
291
+ }
292
+ ```
293
+
294
+ **Problem**:
295
+ - Original error context is lost
296
+ - No traceId attached to new error
297
+ - Global handler receives generic error, can't trace root cause
298
+
299
+ ### GOOD Example 1: Let Error Bubble
300
+
301
+ ```typescript
302
+ // GOOD: Error bubbles to global handler with traceId
303
+ async function processOrder(order: Order): Promise<void> {
304
+ // No try-catch needed!
305
+ await validateOrder(order);
306
+ await saveToDatabase(order);
307
+ // If error occurs, it bubbles with traceId intact
308
+ }
309
+ ```
310
+
311
+ **Why GOOD**:
312
+ - Global handler catches error
313
+ - TraceId from RequestContext is preserved
314
+ - Full stack trace available
315
+ - `/debugLocal/{traceId}` endpoint works
316
+
317
+ ### GOOD Example 2: Global Handler Logs With TraceId
318
+
319
+ ```typescript
320
+ // GOOD: Global handler has full context
321
+ // In WebpiecesMiddleware.globalErrorHandler (see Pattern 1 above)
322
+ catch (err: any) {
323
+ const error = toError(err);
324
+ const traceId = RequestContext.get<string>('TRACE_ID');
325
+
326
+ console.error('[GlobalErrorHandler] ERROR:', {
327
+ traceId, // Unique ID for this request
328
+ message: error.message,
329
+ stack: error.stack,
330
+ path: req.path, // Request context preserved
331
+ });
332
+ }
333
+ ```
334
+
335
+ **Why GOOD**:
336
+ - TraceId logged with every error
337
+ - Full request context available
338
+ - Error stored for `/debugLocal/{id}` lookup
339
+ - DevOps can trace distributed requests
340
+
341
+ ### ACCEPTABLE Example 1: Retry Loop (With eslint-disable)
342
+
343
+ ```typescript
344
+ // ACCEPTABLE: Retry pattern requires try-catch
345
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Retry loop with exponential backoff
346
+ async function callVendorApiWithRetry(request: VendorRequest): Promise<VendorResponse> {
347
+ const maxRetries = 3;
348
+ let lastError: Error | undefined;
349
+
350
+ for (let i = 0; i < maxRetries; i++) {
351
+ try {
352
+ return await vendorApi.call(request);
353
+ } catch (err: any) {
354
+ const error = toError(err);
355
+ lastError = error;
356
+ console.warn(`Retry ${i + 1}/${maxRetries} failed:`, error.message);
357
+ await sleep(1000 * Math.pow(2, i)); // Exponential backoff
358
+ }
359
+ }
360
+
361
+ // After retries exhausted, throw with traceId
362
+ const traceId = RequestContext.get<string>('TRACE_ID');
363
+ throw new HttpVendorError(
364
+ `Vendor API failed after ${maxRetries} retries. TraceId: ${traceId}`,
365
+ lastError
366
+ );
367
+ }
368
+ ```
369
+
370
+ **Why ACCEPTABLE**:
371
+ - Legitimate use case: retry logic
372
+ - Final error still includes traceId
373
+ - Error still bubbles to global handler
374
+ - Requires senior developer approval (enforced by PR review)
375
+
376
+ ### ACCEPTABLE Example 2: Batching Pattern (With eslint-disable)
377
+
378
+ ```typescript
379
+ // ACCEPTABLE: Batching requires try-catch to continue processing
380
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Batch processing continues on individual failures
381
+ async function processBatch(items: Item[]): Promise<BatchResult> {
382
+ const results: ItemResult[] = [];
383
+ const errors: ItemError[] = [];
384
+ const traceId = RequestContext.get<string>('TRACE_ID');
385
+
386
+ for (const item of items) {
387
+ try {
388
+ const result = await processItem(item);
389
+ results.push(result);
390
+ } catch (err: any) {
391
+ const error = toError(err);
392
+ // Log individual error with traceId
393
+ console.error(`[Batch] Item ${item.id} failed (traceId: ${traceId}):`, error);
394
+ errors.push({ itemId: item.id, error: error.message, traceId });
395
+ }
396
+ }
397
+
398
+ // Return both successes and failures
399
+ return {
400
+ traceId,
401
+ successCount: results.length,
402
+ failureCount: errors.length,
403
+ results,
404
+ errors,
405
+ };
406
+ }
407
+ ```
408
+
409
+ **Why ACCEPTABLE**:
410
+ - Legitimate use case: partial failure handling
411
+ - Each error logged with traceId
412
+ - Batch traceId included in response
413
+ - Requires senior developer approval (enforced by PR review)
414
+
415
+ ### UNACCEPTABLE Example: Pointless Try-Catch-Rethrow (Internal Code)
416
+
417
+ ```typescript
418
+ // UNACCEPTABLE: Pointless try-catch in INTERNAL code
419
+ async function saveUser(user: User): Promise<void> {
420
+ try {
421
+ await userRepository.save(user); // Internal call, not edge
422
+ } catch (err: any) {
423
+ const error = toError(err);
424
+ console.error('Save failed:', error);
425
+ throw error; // No value added - why catch?
426
+ }
427
+ }
428
+ ```
429
+
430
+ **Why UNACCEPTABLE for internal code**:
431
+ - Adds no value - logging should be in LogApiFilter
432
+ - Global handler already logs errors
433
+ - Just adds noise and confusion
434
+ - Remove the try-catch entirely!
435
+
436
+ **CONTRAST with edge code (ACCEPTABLE)**:
437
+ ```typescript
438
+ // ACCEPTABLE: Edge code calling external database service
439
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Edge code: database logging
440
+ async function saveUserToDb(user: User): Promise<void> {
441
+ const traceId = RequestContext.get<string>('TRACE_ID');
442
+ try {
443
+ logRequest('[DB] Saving user', { traceId, userId: user.id });
444
+ await externalDbClient.save('users', user); // EDGE: external service
445
+ logSuccess('[DB] User saved', { traceId, userId: user.id });
446
+ } catch (err: any) {
447
+ const error = toError(err);
448
+ logFailure('[DB] Save failed', { traceId, userId: user.id, error: error.message });
449
+ throw error; // Rethrow - logging value at the edge
450
+ }
451
+ }
452
+ ```
453
+
454
+ **The difference**: Edge code benefits from request/response/failure logging at the service boundary. Internal code does not.
455
+
456
+ ## When eslint-disable IS Acceptable
457
+
458
+ You may use `// eslint-disable-next-line @webpieces/no-unmanaged-exceptions` ONLY for:
459
+
460
+ 1. **Retry loops** with exponential backoff (vendor API calls)
461
+ 2. **Batching patterns** where partial failure is expected
462
+ 3. **Resource cleanup** with explicit approval
463
+ 4. **Global error handler entry points** (see below)
464
+ 5. **Edge code patterns** for vendor/external service calls (see below)
465
+ 6. **Form error handling** - catching `HttpUserError` for display, rethrowing others (see below)
466
+
467
+ All require:
468
+ - Comment explaining WHY try-catch is needed
469
+ - TraceId must still be logged/included in final error (or error must be rethrown)
470
+
471
+ ## Global Try-Catch Entry Points (MUST ASK USER)
472
+
473
+ **CRITICAL: 95% of the time, the code you're looking at is NOT a global entry point!**
474
+
475
+ 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?"
476
+
477
+ ### Examples of LEGITIMATE Global Error Handlers
478
+
479
+ These are the rare places where global try-catch IS correct:
480
+
481
+ 1. **Node.js/Express middleware** (at the TOP, after setting up traceId in context):
482
+ ```typescript
483
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Global error handler entry point
484
+ app.use(async (req, res, next) => {
485
+ // First: set up traceId in RequestContext
486
+ const traceId = uuidv4();
487
+ RequestContext.put('TRACE_ID', traceId);
488
+
489
+ try {
490
+ await next();
491
+ } catch (err: any) {
492
+ const error = toError(err);
493
+ // Report to Sentry/observability
494
+ Sentry.captureException(error, { extra: { traceId } });
495
+ res.status(500).json({ error: 'Internal error', traceId });
496
+ }
497
+ });
498
+ ```
499
+
500
+ 2. **RxJS global error handler**:
501
+ ```typescript
502
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- RxJS global unhandled error hook
503
+ config.onUnhandledError = (err: any) => {
504
+ const error = toError(err);
505
+ Sentry.captureException(error);
506
+ console.error('[RxJS Unhandled]', error);
507
+ };
508
+ ```
509
+
510
+ 3. **Browser window unhandled promise rejection**:
511
+ ```typescript
512
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Browser global unhandled promise handler
513
+ window.addEventListener('unhandledrejection', (event) => {
514
+ const error = toError(event.reason);
515
+ Sentry.captureException(error);
516
+ console.error('[Unhandled Promise]', error);
517
+ });
518
+ ```
519
+
520
+ 4. **Angular ErrorHandler** (may need try-catch to prevent double recording):
521
+ ```typescript
522
+ @Injectable()
523
+ export class GlobalErrorHandler implements ErrorHandler {
524
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Angular global error handler
525
+ handleError(error: any): void {
526
+ try {
527
+ Sentry.captureException(error);
528
+ } catch (sentryError) {
529
+ // Prevent infinite loop if Sentry itself fails
530
+ console.error('[Sentry failed]', sentryError);
531
+ }
532
+ console.error('[Angular Error]', error);
533
+ }
534
+ }
535
+ ```
536
+
537
+ 5. **3rd party vendor event listeners**:
538
+ ```typescript
539
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Vendor callback error boundary
540
+ vendorSdk.on('event', async (data) => {
541
+ try {
542
+ await processVendorEvent(data);
543
+ } catch (err: any) {
544
+ const error = toError(err);
545
+ const traceId = RequestContext.get<string>('TRACE_ID');
546
+ Sentry.captureException(error, { extra: { traceId, vendorData: data } });
547
+ // Don't rethrow - vendor SDK may not handle errors gracefully
548
+ }
549
+ });
550
+ ```
551
+
552
+ **All global handlers should report to observability (Sentry, Datadog, etc.) in production.**
553
+
554
+ ## Edge Code Patterns (MUST ASK USER)
555
+
556
+ 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**.
557
+
558
+ **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?"
559
+
560
+ ### Example: sendMail Pattern
561
+ ```typescript
562
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Edge code: external email service logging
563
+ async function sendMail(request: MailRequest): Promise<MailResponse> {
564
+ const traceId = RequestContext.get<string>('TRACE_ID');
565
+
566
+ try {
567
+ logRequest('[Email] Sending', { traceId, to: request.to, subject: request.subject });
568
+ const response = await emailService.send(request);
569
+ logSuccess('[Email] Sent', { traceId, messageId: response.messageId });
570
+ return response;
571
+ } catch (err: any) {
572
+ const error = toError(err);
573
+ logFailure('[Email] Failed', { traceId, error: error.message, to: request.to });
574
+ throw error; // Rethrow - adds logging value at the edge
575
+ }
576
+ }
577
+ ```
578
+
579
+ ### Why This Pattern Is Valuable at Edges
580
+
581
+ 1. **Complete audit trail**: Request logged, then either success OR failure logged
582
+ 2. **Vendor debugging**: When vendor says "we never received it", you have proof
583
+ 3. **Performance monitoring**: Track timing at service boundaries
584
+ 4. **Correlation**: TraceId connects this edge call to the overall request
585
+
586
+ ### Where Edge Code Patterns Apply
587
+
588
+ - HTTP client calls to external APIs
589
+ - Database operations (especially writes)
590
+ - Message queue publish/consume
591
+ - Email/SMS/notification services
592
+ - Payment gateway calls
593
+ - File storage operations (S3, GCS, etc.)
594
+ - Any call leaving your service boundary
595
+
596
+ ## Form Error Handling Pattern (Client-Side)
597
+
598
+ 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.
599
+
600
+ **This pattern is ACCEPTABLE because**:
601
+ - It catches ONLY user-facing errors (`HttpUserError`) for display
602
+ - Unexpected errors are RETHROWN (not swallowed)
603
+ - Server throws `HttpUserError` → protocol translates to error payload → client translates back to exception
604
+
605
+ ### Example: Form Submission Error Handling
606
+ ```typescript
607
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Form error display: catch user errors, rethrow others
608
+ async submitForm(): Promise<void> {
609
+ try {
610
+ await this.apiClient.saveData(this.formData);
611
+ this.router.navigate(['/success']);
612
+ } catch (err: any) {
613
+ const error = toError(err);
614
+
615
+ if (error instanceof HttpUserError) {
616
+ // User-facing error - display in form
617
+ this.formError = error.message;
618
+ this.cdr.detectChanges();
619
+ } else {
620
+ // Unexpected error - let global handler deal with it
621
+ throw error;
622
+ }
623
+ }
624
+ }
625
+ ```
626
+
627
+ ### Why This Pattern Is Valid
628
+
629
+ 1. **Selective catching**: Only catches errors meant for user display
630
+ 2. **No swallowing**: Unexpected errors bubble to global handler with traceId
631
+ 3. **Protocol design**: Server intentionally throws `HttpUserError` for user-facing messages
632
+ 4. **UX requirement**: Forms must show validation errors inline, not via global error page
633
+
634
+ ### Key Requirements
635
+
636
+ - **ONLY catch specific error types** (e.g., `HttpUserError`, `ValidationError`)
637
+ - **ALWAYS rethrow** errors that aren't user-facing
638
+ - The server-side code should throw `HttpUserError` for user-displayable messages
639
+
640
+ ## How to Request Approval
641
+
642
+ If you believe you have a legitimate use case for try-catch:
643
+
644
+ 1. **Add a comment** explaining why it's needed:
645
+ ```typescript
646
+ // JUSTIFICATION: Vendor API requires retry loop with exponential backoff
647
+ // to handle rate limiting. Final error includes traceId for debugging.
648
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
649
+ ```
650
+
651
+ 2. **Ensure traceId is preserved** in final error or logged
652
+
653
+ 3. **Request PR review** from senior developer
654
+
655
+ 4. **Be prepared to justify** - 99% of try-catch can be removed
656
+
657
+ ## Summary
658
+
659
+ **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.
660
+
661
+ **Key takeaways**:
662
+ - Global error handler with traceId = debuggable production issues
663
+ - Local try-catch in internal code = lost context and debugging nightmares
664
+ - 95% of try-catch blocks can be removed safely
665
+ - Acceptable try-catch uses: retries, batching, global entry points, edge code
666
+ - **AI agents MUST ask user** before adding global try-catch or edge code patterns
667
+ - TraceId enables `/debugLocal/{id}` and `/debugCloud/{id}` endpoints
668
+
669
+ **Acceptable patterns (with eslint-disable)**:
670
+ 1. **Global entry points**: Express middleware, RxJS error hooks, Angular ErrorHandler, browser unhandledrejection
671
+ 2. **Edge code**: External API calls, database operations, email services - use logRequest/logSuccess/logFailure pattern
672
+ 3. **Retry loops**: Vendor APIs with exponential backoff
673
+ 4. **Batching**: Partial failure handling where processing must continue
674
+ 5. **Form error handling**: Catch `HttpUserError` for UI display, rethrow all other errors
675
+
676
+ **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.
677
+