Uh oh!
There was an error while loading. Please reload this page.
[eslint-miner] feat(eslint): add no-caught-error-interpolation rule - #48175
Conversation
Adds a new custom ESLint rule that detects bare caught-error variable
interpolation in template literals (e.g. `...${err}...`).
Problem: Directly interpolating a caught error variable in a template
literal is unreliable:
- For Error objects: produces redundant 'Error: message' prefix
- For non-Error throws: produces useless '[object Object]'
The rule flags bare Identifier expressions in template literals when the
variable is a caught error param (try/catch, .catch(), or .then rejection
handler). It suggests using getErrorMessage(err) if available or String(err)
as an import-free alternative.
Real instances found and flagged:
- actions/setup/js/copilot_sdk_driver.cjs:87
- actions/setup/js/copilot_harness.cjs:850
Not flagged (intentionally excluded):
- err.message, err.stack, err.toString() — covered by no-unsafe-catch-error-property
- String(err), getErrorMessage(err) — already correct forms
- Outer catch vars inside non-rejection function callbacks (false-positive guard)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #48175 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100). |
✅ PR Code Quality Reviewer completed the code quality review. |
✅ Test Quality Sentinel completed test quality analysis. |
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
There was a problem hiding this comment.
Pull request overview
Adds an ESLint rule to detect direct interpolation of caught errors in template literals.
Changes:
- Implements and registers
no-caught-error-interpolation. - Adds rule tests and enables it as a warning.
- Provides formatting replacement suggestions.
Show a summary per file
| File | Description |
|---|---|
eslint-factory/src/rules/no-caught-error-interpolation.ts | Implements detection and suggestions. |
eslint-factory/src/rules/no-caught-error-interpolation.test.ts | Tests supported patterns. |
eslint-factory/src/index.ts | Registers the rule. |
eslint-factory/eslint.config.cjs | Enables the rule. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (2)
eslint-factory/src/rules/no-caught-error-interpolation.ts:164
String(errorVar)does not remediate the reported behavior: template-literal interpolation already performs string coercion, so both forms produce"Error: message"forErrorand"[object Object]"for a plain object. Applying this suggestion therefore silences the rule without improving the output. OffergetErrorMessageonly (or another formatter that actually handles unknown thrown values), and update the fallback message/tests accordingly.
: ({
messageId: "useStringFallback" as const,
data: { errorVar },
fix(fixer) {
return fixer.replaceText(expr, `String(${errorVar})`);
},
} as const),
eslint-factory/src/rules/no-caught-error-interpolation.ts:140
- Matching only the identifier name reports shadowing locals as caught errors. For example,
catch (err) { { const err = "formatted"; log(${err}); } }is flagged even though the interpolated identifier resolves to the inner string. Verify that the expression's resolved variable is the tracked catch/rejection parameter rather than comparing names.
for (const expr of node.expressions) {
if (!isBareIdentifierExpression(expr)) continue;
if (!caughtNames.has(expr.name)) continue;
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Medium
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
🧪 Test Quality Sentinel Report
📊 Metrics (19 test cases)
✅ Test StrengthsComprehensive Coverage: 11 valid test cases establish what should NOT be flagged (non-caught vars, alternative accessors, scope boundaries). 8 invalid cases verify rule detection and enforce suggestion/fixer output correctness. Edge Cases: Tests cover try/catch binding, .catch() rejection handlers, .then() rejection handlers, nested non-rejection callbacks, template literals with multiple interpolations, and both CommonJS and ES module contexts. Assertion Quality: All invalid cases verify both (1) error messageId and (2) suggestion/fixer output, enforcing end-to-end fix correctness. No mocking of internal rule logic. Test Inflation: 241 test lines / 171 production lines = 1.41:1 ratio. Acceptable for ESLint rule testing, where comprehensive valid/invalid coverage is standard practice.
|
There was a problem hiding this comment.
Review: no-caught-error-interpolation ESLint rule
Overall the rule is well-designed and the test coverage is solid. One correctness gap needs fixing before merge.
Blocking issue:FunctionDeclaration nodes are not handled as sentinels in the scope stack (see inline comment on line 101). Named functions declared inside a catch block will cause false positives because the outer catch variable stays visible across the sentinel boundary that should have been inserted.
Non-blocking observations:
- The single-suggestion design (only
getErrorMessageORString, not both) is clean and intentional. isDefinitionAvailableAtNodecorrectly handles hoistedFunctionNamebindings.- Test coverage is thorough for the handled cases.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 28.5 AIC · ⌖ 4.63 AIC · ⊞ 5K
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Request changes — rule logic has medium correctness issues and misleading documentation that should be fixed before merge.
### Findings summary
Medium — false negatives (silent misses)
The sentinel heuristic blocks reporting for any non-rejection callback (forEach, map, setTimeout, etc.) nested inside a catch block. Caught errors interpolated in these callbacks produce no warning. This is a significant coverage gap for a rule whose purpose is reliable detection. If intentional, it must be documented explicitly in docs.description.
Medium — false positives on non-Promise calleesisInlineRejectionHandler fires on any .catch(fn) or .then(_, fn) method call regardless of the receiver type, which will produce false positives with RxJS, Chai, or any custom object with those method names. Needs documentation or a narrower check.
Low — wrong JSDoc on isBareIdentifierExpression
The function comment describes a TemplateLiteral-in-TemplateLiteral check; the implementation checks Identifier. Pure documentation bug but confusing for maintainers.
Low — misleading test comment
Test says outer err is "not accessible" inside setTimeout; it is accessible via closure. The sentinel suppresses the report as a deliberate heuristic, not a scoping rule. Wrong mental model baked into test comments.
🔎 Code quality review by PR Code Quality Reviewer · sonnet46 · 49.3 AIC · ⌖ 5.14 AIC · ⊞ 5.7K
Comment /review to run again
Comments that could not be inline-anchored
eslint-factory/src/rules/no-caught-error-interpolation.ts:319
Wrong JSDoc: the isBareIdentifierExpression comment says "Returns true when node is a TemplateLiteral expression inside a TemplateLiteral expression" — that is completely wrong and describes a different function. The implementation just checks node.type === AST_NODE_TYPES.Identifier.
<details>
<summary>💡 Fix</summary>
Replace the JSDoc with an accurate description:
/***Returnstruewhen`expr`isabare`Identifier`node—i.e.thetemplate*expressionis…</details><details><summary>eslint-factory/src/rules/no-caught-error-interpolation.ts:371</summary>**Silentfalsenegativesfor any non-rejectioncallback**: caughterrorvariablesinterpolatedinside`arr.map()`,`forEach()`,`setTimeout()`,oranycallbackthatisn't`.catch(fn)`/`.then(_, fn)`aresilentlyskipped—nolintwarning is emitted.<details><summary>💡Details</summary>When`enterFunction`encountersafunctionthatisNOTaninlinerejectionhandler,itpushesasentinelthatstops`getCaughtVarNames()`fromlookingfurtherupthestack.This means:
```jstry { f();…</details><details><summary>eslint-factory/src/rules/no-caught-error-interpolation.ts:301</summary>**False positives on non-Promise `.catch()`/`.then()` calls**: `isInlineRejectionHandler` treats any method named `catch` or `then` as a Promise rejection handler, regardless of the receiver type.<details><summary>💡 Details</summary>Examples that will be incorrectly flagged:```js// RxJS observableobservable.catch(err=>log(`rx error: ${err}`));// Chai assertion chain (.then is defined on thenables in test frameworks)expect(val).to.not.throw().then(err=> ...);// Any custom objec…</details><details><summary>eslint-factory/src/rules/no-caught-error-interpolation.test.ts:109</summary>**Misleadingtestcomment**: thecommentsays`err`is"notaccessible"insidethe`setTimeout`callback,butthat is factuallywrong—JavaScriptclosuresDOcapturetheouter`catch (err)`binding.Therulesuppressesthereportviathesentinelheuristic,notbecauseofscopingrules.<details><summary>💡Fix</summary>Updatethecommenttoreflectreality:
```typescript// The setTimeout callback is treated as a sentinel (non-rejection function),// so the rule intentionally does NOT re…</details>There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /tdd — no blocking issues, but 5 test-coverage gaps worth closing.
📋 Key Themes & Highlights
Key Themes
- Misleading JSDoc:
isBareIdentifierExpressionhas a comment describing aTemplateLiteralcontext that doesn't match its actual behaviour (checks forIdentifier). - Missing edge-case tests: 4 code paths are untested — zero-param
.catch()handler, destructuredcatchparam, ESMimportofgetErrorMessage, and therangeguard inisDefinitionAvailableAtNode. - Defensive guard missing:
isDefinitionAvailableAtNodeindexesrange[0]without checkingrangeis defined — low-probability but worth a one-line guard.
Positive Highlights
- ✅ Well-scoped sentinel mechanism cleanly avoids false positives for outer catch vars in non-rejection callbacks
- ✅ Context-aware suggestion (prefers
getErrorMessageoverString) is a nice ergonomic touch - ✅ Solid coverage of the happy-path cases across both CJS and ESM contexts
- ✅ PR description is clear: problem, rule behavior, real instances, and exclusions all documented
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 49 AIC · ⌖ 5.15 AIC · ⊞ 6.7K
Comment /matt to run again
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
gh-aw-bot
commented
Jul 26, 2026
@copilot please run the Unresolved review feedback (newest first):
|
… rule - Replace stack-based scope tracking with ESLint scope analysis so closures retain access to outer catch bindings (fixes false negatives from non-rejection function sentinels) - Skip TaggedTemplateExpression nodes — tag functions receive raw values, not string-coerced output - Fix isBareIdentifierExpression JSDoc to accurately describe the function - Add isCaughtErrorVariableDef helper that correctly identifies catch clause bindings (simple identifier only, not destructured params) and inline rejection handler params via scope analysis - Guard definitionNode.range being undefined in isDefinitionAvailableAtNode - Remove FunctionDeclaration / FunctionExpression / ArrowFunctionExpression visitors — no longer needed with scope-based approach - Add tests: tagged template (valid), zero-param .catch() (valid), destructured catch params (valid), closures flagging outer catch var (invalid), ESM import getErrorMessage suggestion path (invalid) Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
All nine items addressed in commits Copilot review threads:
GitHub Actions review threads:
|
gh-aw-bot
commented
Jul 26, 2026
@copilot please run the Recent author follow-up: Unresolved review feedback still showing on this PR (newest first):
Please cover code, tests, and GitHub thread state in one pass.
|
All 9 threads have been addressed in commits Thread-by-thread verification against current code:
Remaining blockers for maintainers:
|
gh-aw-bot
commented
Jul 26, 2026
gh-aw-bot
commented
Jul 26, 2026
All 9 review threads are already resolved on GitHub — confirmed via the review threads API, every thread shows Status:
Hand-off: Ready for human CI re-trigger and merge. |
Investigated all 9 listed thread IDs. After tracing the PR Sous Chef's processing logs, I can confirm that the thread IDs Verified current state:
Resolved threads (all 9):
This PR is ready for maintainer merge. CI must be re-triggered after merge (close/reopen or |
gh-aw-bot
commented
Jul 26, 2026
@copilot please run the Recent review signals to verify:
|
Verified all 4 github-actions review signals (Test Quality Sentinel, Impeccable Skills Reviewer, PR Code Quality Reviewer, Matt Pocock Skills Reviewer — all originally on commit Current state:
No code changes were needed in this pass. This PR is ready for maintainer CI re-trigger and merge. |
gh-aw-bot
commented
Jul 26, 2026
@copilot please run the If no code changes are needed, say so explicitly and call out whether this PR only needs thread cleanup and/or fresh CI on the current head. Run: https://github.com/github/gh-aw/actions/runs/30216617510
|
No code changes are needed. Current state (verified against GitHub API):
Only remaining maintainer action: re-trigger CI on the current HEAD (close/reopen PR or |
gh-aw-bot
commented
Jul 26, 2026
@copilot please run the
|
No code changes are needed. Current state confirmed via GitHub API:
Only remaining action: a maintainer must re-trigger CI (close/reopen or |
Uh oh!
There was an error while loading. Please reload this page.
🎉 This pull request is included in a new release. Release: |
Summary
Adds a new ESLint rule
no-caught-error-interpolationto thegh-aw-customplugin. The rule flags bare caught-error variable interpolation in template literals, which silently produces[object Object]for non-Error throws or the redundantError: messageprefix for Error instances.Changes
New rule —
eslint-factory/src/rules/no-caught-error-interpolation.tsIdentifierexpressions (no member access, no call) inside untagged template literals where the identifier resolves — through the full scope chain, including closures — to a caught-error binding:try/catchclause with a simpleIdentifierparam (destructured params are excluded)..catch(fn)or.then(onFulfilled, fn).bareErrorInterpolationwith a single autofixable suggestion:useGetErrorMessage— wraps withgetErrorMessage(err)whengetErrorMessageis resolvable in scope.useStringFallback— wraps withString(err)otherwise.err.message/err.stackmember access, and safe call forms (String(err),getErrorMessage(err),err.toString()).isDefinitionAvailableAtNodeguards against synthetic/virtual nodes that have norange, avoiding runtime throws.Registration
eslint-factory/src/index.ts: imports and registersnoCaughtErrorInterpolationRulein the plugin rules map.eslint-factory/eslint.config.cjs: enables the rule atwarnseverity.Tests —
eslint-factory/src/rules/no-caught-error-interpolation.test.tsFull Vitest +
RuleTestersuite covering CJS and ESM source types:getErrorMessage(err),String(err),err.message/err.stack,err.toString(), tagged templates, zero-param.catch(), destructured catch paramsgetErrorMessagesuggestion when in scope,.catch()/.then()rejection handlers, multiple interpolations in same template, closures (arrow, function expression,FunctionDeclaration) inside catch body, ESM import contextCommits
3bb7482680b7f3f1f9f149cafd802d39b5de