From 581a708f2006a89c4f95e06bb5566f6c53a7937c Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Thu, 3 Sep 2026 11:40:23 +0100 Subject: [PATCH] fix: provide more detailed error causes on fatal failures --- .../cli-integ/resources/cdk-apps/app/app.js | 10 ++-- .../language-tests/integ.typescript-test.ts | 2 +- .../lib/api/hotswap/cloud-control-resource.ts | 15 ++++-- .../lib/api/hotswap/hotswap-deployments.ts | 6 ++- packages/aws-cdk/lib/cli/cdk-toolkit.ts | 4 +- .../aws-cdk/lib/cli/pretty-print-error.ts | 51 ++++++++++++++----- ...led_resource_deployment_with_cause.ndjson} | 0 packages/aws-cdk/test/commands/deploy.test.ts | 8 ++- 8 files changed, 66 insertions(+), 30 deletions(-) rename packages/aws-cdk/test/commands/__io_snapshots__/deploy/{deploy_failures_wraps_a_failed_resource_deployment_as_stack_failed_error_and_rethrows.ndjson => deploy_failures_wraps_a_failed_resource_deployment_with_cause.ndjson} (100%) diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js index 7163a3951..24c6f0635 100755 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -167,8 +167,8 @@ class ListMultipleDependentStack extends Stack { const dependentStack1 = new DependentStack1(this, 'DependentStack1'); const dependentStack2 = new DependentStack2(this, 'DependentStack2'); - this.addDependency(dependentStack1); - this.addDependency(dependentStack2); + this.addStackDependency(dependentStack1); + this.addStackDependency(dependentStack2); } } @@ -192,7 +192,7 @@ class ListStack extends Stack { const dependentStack = new DependentStack(this, 'DependentStack'); - this.addDependency(dependentStack); + this.addStackDependency(dependentStack); } } @@ -202,7 +202,7 @@ class DependentStack extends Stack { const innerDependentStack = new InnerDependentStack(this, 'InnerDependentStack'); - this.addDependency(innerDependentStack); + this.addStackDependency(innerDependentStack); } } @@ -1162,7 +1162,7 @@ switch (stackSet) { // A stack that depends on the failed stack -- used to test that '-e' does not deploy the failing stack const dependsOnFailed = new OutputsStack(app, `${stackPrefix}-depends-on-failed`); - dependsOnFailed.addDependency(failed); + dependsOnFailed.addStackDependency(failed); if (process.env.ENABLE_VPC_TESTING) { // Gating so we don't do context fetching unless that's what we are here for const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }; diff --git a/packages/@aws-cdk/integ-runner/test/language-tests/integ.typescript-test.ts b/packages/@aws-cdk/integ-runner/test/language-tests/integ.typescript-test.ts index 6195072f4..ed1ec3933 100644 --- a/packages/@aws-cdk/integ-runner/test/language-tests/integ.typescript-test.ts +++ b/packages/@aws-cdk/integ-runner/test/language-tests/integ.typescript-test.ts @@ -16,7 +16,7 @@ const queue = new cdk.CfnResource(stack, 'Queue', { }); const assertionStack = new cdk.Stack(app, 'TypeScriptAssertions'); -assertionStack.addDependency(stack); +assertionStack.addStackDependency(stack); const integ = new IntegTest(app, 'TypeScript', { testCases: [stack], diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/hotswap/cloud-control-resource.ts b/packages/@aws-cdk/toolkit-lib/lib/api/hotswap/cloud-control-resource.ts index 988803774..9e375cd24 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/hotswap/cloud-control-resource.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/hotswap/cloud-control-resource.ts @@ -2,6 +2,7 @@ import type { HotswapChange } from './common'; import { classifyChanges, nonHotswappableChange } from './common'; import { NonHotswappableReason } from '../../payloads'; import type { ResourceChange } from '../../payloads/hotswap'; +import { ToolkitError } from '../../toolkit/toolkit-error'; import type { SDK } from '../aws-auth/private'; import { CfnEvaluationException, type EvaluateCloudFormationTemplate } from '../cloudformation'; @@ -92,11 +93,15 @@ export async function isHotswappableCloudControlChange( return; } - await cloudControl.updateResource({ - TypeName: resourceType, - Identifier: identifier, - PatchDocument: JSON.stringify(patchOps), - }); + try { + await cloudControl.updateResource({ + TypeName: resourceType, + Identifier: identifier, + PatchDocument: JSON.stringify(patchOps), + }); + } catch (e) { + throw ToolkitError.withCause('HotswapFailed', `Failed to update ${identifier} (${resourceType})`, e); + } }, }); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/hotswap/hotswap-deployments.ts b/packages/@aws-cdk/toolkit-lib/lib/api/hotswap/hotswap-deployments.ts index 706f06d1e..ab3ef3f3e 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/hotswap/hotswap-deployments.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/hotswap/hotswap-deployments.ts @@ -576,10 +576,12 @@ async function applyHotswapOperation(sdk: SDK, ioSpan: IMessageSpan, hotswa } catch (e: any) { if (e.name === 'TimeoutError' || e.name === 'AbortError') { const result: WaiterResult = JSON.parse(formatErrorMessage(e)); - const error = new ToolkitError('HotswapWaiterFailed', formatWaiterErrorResult(result)); - error.name = e.name; + const error = ToolkitError.withCause('HotswapWaiterFailed', `[${hotswapOperation.service}] ` + formatWaiterErrorResult(result), e); throw error; } + + // Always prepend the hotswap service to errors + e.message = `[${hotswapOperation.service}] ${e.message}`; throw e; } diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index dd9413e79..06d91ccfa 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -2576,8 +2576,8 @@ class WorkGraphDeploymentActions implements WorkGraphActions { // It has to be exactly this string because an integration test tests for // "bold(stackname) failed: ResourceNotReady: " const code = ToolkitError.isToolkitError(e) ? e.name : 'DeployStackFailed'; // Formerly 'DeployFailed' - const newMessage = [`❌ ${chalk.bold(stack.stackName)} failed:`, ...(e.name ? [`${e.name}:`] : []), e.message].join(' '); - const wrappedError = new ToolkitError(code, newMessage); + const newMessage = `❌ ${chalk.bold(stack.stackName)} failed to deploy`; + const wrappedError = ToolkitError.withCause(code, newMessage, e); error = { name: cdkCliErrorName(wrappedError), diff --git a/packages/aws-cdk/lib/cli/pretty-print-error.ts b/packages/aws-cdk/lib/cli/pretty-print-error.ts index eee62a4c0..7a96001fb 100644 --- a/packages/aws-cdk/lib/cli/pretty-print-error.ts +++ b/packages/aws-cdk/lib/cli/pretty-print-error.ts @@ -1,32 +1,57 @@ /* eslint-disable no-console */ import chalk from 'chalk'; +interface PrettyErrorPrinterOptions { + /** + * Print the error as an expected outcome, for example when a user declined a confirmation prompt. + * While thrown as exceptions, these should visually not be presented as a crash. + */ + readonly soft: boolean; + /** + * Prints as much debug output as possible. + */ + readonly debug: boolean; +} + /* c8 ignore start */ -export function prettyPrintError(error: unknown, options: { soft?: boolean; debug?: boolean } = {}) { +export function prettyPrintError(error: unknown, options: PrettyErrorPrinterOptions = { soft: false, debug: false }) { const err = ensureError(error); - const debug = options.debug ?? false; - const soft = options.soft ?? false; // A soft error (for example a user-declined confirmation) is an expected outcome, not a crash. // Present the message less scary. - const errorPaint = soft ? chalk.yellow : chalk.red; + const errorPaint = options.soft ? chalk.yellow : chalk.red; console.error(errorPaint(err.message)); - if (err.cause && !soft) { - const cause = ensureError(err.cause); - console.error(chalk.yellow(cause.message)); - printTrace(cause, debug); + printCauses(err, options); + + // Log the stack trace if we're on a developer workstation. Otherwise this will be into a minified + // file and the printed code line and stack trace are huge and useless. + if (options.debug) { + printTraces(err); } +} - printTrace(err, debug); +/** + * Recursively print all error causes recursively. + */ +function printCauses(err: Error, options: PrettyErrorPrinterOptions) { + if (err.cause && !options.soft) { + const cause = ensureError(err.cause); + console.error(chalk.yellow(`‣ ${cause.name}: ${cause.message}`)); + printCauses(cause, options); + } } -function printTrace(err: Error, debug = false) { - // Log the stack trace if we're on a developer workstation. Otherwise this will be into a minified - // file and the printed code line and stack trace are huge and useless. - if (err.stack && debug) { +/** + * Recursively print all error traces. + */ +function printTraces(err: Error) { + if (err.stack) { console.debug(chalk.gray(err.stack)); } + if (err.cause) { + printTraces(ensureError(err.cause)); + } } function ensureError(value: unknown): Error { diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/deploy/deploy_failures_wraps_a_failed_resource_deployment_as_stack_failed_error_and_rethrows.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/deploy/deploy_failures_wraps_a_failed_resource_deployment_with_cause.ndjson similarity index 100% rename from packages/aws-cdk/test/commands/__io_snapshots__/deploy/deploy_failures_wraps_a_failed_resource_deployment_as_stack_failed_error_and_rethrows.ndjson rename to packages/aws-cdk/test/commands/__io_snapshots__/deploy/deploy_failures_wraps_a_failed_resource_deployment_with_cause.ndjson diff --git a/packages/aws-cdk/test/commands/deploy.test.ts b/packages/aws-cdk/test/commands/deploy.test.ts index 27e67c8fe..208ef63b7 100644 --- a/packages/aws-cdk/test/commands/deploy.test.ts +++ b/packages/aws-cdk/test/commands/deploy.test.ts @@ -542,7 +542,7 @@ describe('deploy parameters forwarded to CloudFormation', () => { }); describe('deploy failures', () => { - test('wraps a failed resource deployment as " failed: " and rethrows', async () => { + test('wraps a failed resource deployment with cause', async () => { // Error messages are not emitted to the IoHost, so the snapshot // shows the deploy stopping mid-flight rather than a failure line. const resourceFailure = Object.assign( @@ -558,7 +558,11 @@ describe('deploy failures', () => { }).catch((e) => e); expect(stripAnsi(error.message)).toBe( - '❌ Test-Stack-A failed: ResourceNotReady: Resource TemplateName did not stabilize (reason: CREATE_FAILED)', + '❌ Test-Stack-A failed to deploy', + ); + expect(stripAnsi(error.cause.name)).toBe('ResourceNotReady'); + expect(stripAnsi(error.cause.message)).toContain( + 'Resource TemplateName did not stabilize (reason: CREATE_FAILED)', ); expect(error.name).toBe('DeployStackFailed'); });