Skip to content

Commit edf7906

Browse files
committed
fix(error): preserve Error.cause chain in non-debug output
Addresses Cursor bugbot feedback on PR #1238. The previous code walked the cause chain only when `showStack`/verbose was set, so non-debug users lost the wrapped-error diagnostic context that `messageWithCauses` used to concatenate automatically. - Always fold up to 5 `Error.cause` messages into `message` for generic Error instances, so the outer message reads like `outer: middle: root` — matching `pony-cause`'s behavior. - The verbose/debug path still renders the richer `Caused by [N]:` formatted body with stack traces. - Add regression tests covering both the cause-chain preservation and the 5-level truncation cap.
1 parent a4c5db8 commit edf7906

File tree

2 files changed

+48
-0
lines changed

2 files changed

+48
-0
lines changed

packages/cli/src/utils/error/display.mts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,23 @@ export function formatErrorForDisplay(
7474
body = error.body
7575
} else if (error instanceof Error) {
7676
title = opts.title || 'Unexpected error'
77+
// Concatenate the cause chain into `message` (what non-debug users see)
78+
// so diagnostic context from wrapped errors isn't silently dropped.
79+
// `showStack` adds a richer formatted body with stack traces below.
7780
message = error.message
81+
const plainCauses: string[] = []
82+
let walk: unknown = error.cause
83+
let walkDepth = 1
84+
while (walk && walkDepth <= 5) {
85+
plainCauses.push(
86+
walk instanceof Error ? walk.message : String(walk),
87+
)
88+
walk = walk instanceof Error ? walk.cause : undefined
89+
walkDepth++
90+
}
91+
if (plainCauses.length) {
92+
message = `${message}: ${plainCauses.join(': ')}`
93+
}
7894

7995
if (showStack && error.stack) {
8096
// Format stack trace with proper indentation.

packages/cli/test/unit/utils/error/display.test.mts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,38 @@ describe('error/display', () => {
150150
expect(result.message).toBe('Something went wrong')
151151
})
152152

153+
it('preserves Error.cause chain in message without debug mode', () => {
154+
// Regression: formatErrorForDisplay used to only surface causes
155+
// under showStack/verbose, so non-debug users lost the most useful
156+
// diagnostic context. See PR #1238.
157+
const inner = new Error('root DNS failure')
158+
const middle = new Error('network call failed', { cause: inner })
159+
const outer = new Error('API request failed', { cause: middle })
160+
161+
const result = formatErrorForDisplay(outer)
162+
163+
expect(result.message).toContain('API request failed')
164+
expect(result.message).toContain('network call failed')
165+
expect(result.message).toContain('root DNS failure')
166+
})
167+
168+
it('stops walking causes at depth 5 to avoid runaway chains', () => {
169+
// Build inside-out so the outer Error sits at index 10 and chains
170+
// down through level-9, level-8, ..., level-0.
171+
let e: Error | undefined
172+
for (let i = 0; i <= 10; i++) {
173+
e = new Error(`level-${i}`, e ? { cause: e } : undefined)
174+
}
175+
176+
const result = formatErrorForDisplay(e!)
177+
178+
// Top message + 5 causes should appear.
179+
expect(result.message).toContain('level-10')
180+
expect(result.message).toContain('level-5')
181+
// Anything beyond depth 5 should have been truncated.
182+
expect(result.message).not.toContain('level-4')
183+
})
184+
153185
it('uses custom title when provided', () => {
154186
const error = new Error('Something went wrong')
155187

0 commit comments

Comments
 (0)