Skip to content

Commit 2607eb3

Browse files
authored
fix(lambda): improve validation errors for lambda functions (#32323)
### Issue # (if applicable) Relates to #32324 ### Reason for this change Currently all errors are untyped. This makes it difficult users to programmatically distinguish between different classes of errors, e.g. what is a validation error vs what is a syntax error? With this change, users can catch errors and check their type before proceeding accordingly. ### Description of changes Addition of a new Error type `ValidationError`. For now this error is used only in a single file. The intention is to extend this to all error cases. `ValidationError` extends an abstract `ConstructError` which also handles any improvements to error display. `ConstructError` manipulates the stack trace to improve display. It's changing two things, both of which are based on a construct that is passed in on error creation. If not construct is passed, the error behaves as before. 1. Construct information is inserted as the first line of the stack trace. 2. The strack trace is captured from the point of _creation of the construct_. That is the class constructor call. This is achieved by passing the error's constructs into [Error.captureStackTrace](https://nodejs.org/docs/latest-v22.x/api/errors.html#errorcapturestacktracetargetobject-constructoropt). As a side effect, in many cases the "line of error" is not minified code anymore and thus doesn't ruin the error experience for users. See comments for current vs future errors. ### Description of how you validated changes Existing test. Manual testing of error cases. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 609faba commit 2607eb3

File tree

4 files changed

+224
-43
lines changed

4 files changed

+224
-43
lines changed

packages/aws-cdk-lib/aws-lambda/lib/function.ts

+44-43
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import * as logs from '../../aws-logs';
3030
import * as sns from '../../aws-sns';
3131
import * as sqs from '../../aws-sqs';
3232
import { Annotations, ArnFormat, CfnResource, Duration, FeatureFlags, Fn, IAspect, Lazy, Names, Size, Stack, Token } from '../../core';
33+
import { ValidationError } from '../../core/lib/errors';
3334
import { LAMBDA_RECOGNIZE_LAYER_VERSION } from '../../cx-api';
3435

3536
/**
@@ -917,16 +918,16 @@ export class Function extends FunctionBase {
917918

918919
if (props.functionName && !Token.isUnresolved(props.functionName)) {
919920
if (props.functionName.length > 64) {
920-
throw new Error(`Function name can not be longer than 64 characters but has ${props.functionName.length} characters.`);
921+
throw new ValidationError(`Function name can not be longer than 64 characters but has ${props.functionName.length} characters.`, this);
921922
}
922923
if (!/^[a-zA-Z0-9-_]+$/.test(props.functionName)) {
923-
throw new Error(`Function name ${props.functionName} can contain only letters, numbers, hyphens, or underscores with no spaces.`);
924+
throw new ValidationError(`Function name ${props.functionName} can contain only letters, numbers, hyphens, or underscores with no spaces.`, this);
924925
}
925926
}
926927

927928
if (props.description && !Token.isUnresolved(props.description)) {
928929
if (props.description.length > 256) {
929-
throw new Error(`Function description can not be longer than 256 characters but has ${props.description.length} characters.`);
930+
throw new ValidationError(`Function description can not be longer than 256 characters but has ${props.description.length} characters.`, this);
930931
}
931932
}
932933

@@ -951,10 +952,10 @@ export class Function extends FunctionBase {
951952
const config = props.filesystem.config;
952953
if (!Token.isUnresolved(config.localMountPath)) {
953954
if (!/^\/mnt\/[a-zA-Z0-9-_.]+$/.test(config.localMountPath)) {
954-
throw new Error(`Local mount path should match with ^/mnt/[a-zA-Z0-9-_.]+$ but given ${config.localMountPath}.`);
955+
throw new ValidationError(`Local mount path should match with ^/mnt/[a-zA-Z0-9-_.]+$ but given ${config.localMountPath}.`, this);
955956
}
956957
if (config.localMountPath.length > 160) {
957-
throw new Error(`Local mount path can not be longer than 160 characters but has ${config.localMountPath.length} characters.`);
958+
throw new ValidationError(`Local mount path can not be longer than 160 characters but has ${config.localMountPath.length} characters.`, this);
958959
}
959960
}
960961
if (config.policies) {
@@ -1019,16 +1020,16 @@ export class Function extends FunctionBase {
10191020
}
10201021

10211022
if (props.architecture && props.architectures !== undefined) {
1022-
throw new Error('Either architecture or architectures must be specified but not both.');
1023+
throw new ValidationError('Either architecture or architectures must be specified but not both.', this);
10231024
}
10241025
if (props.architectures && props.architectures.length > 1) {
1025-
throw new Error('Only one architecture must be specified.');
1026+
throw new ValidationError('Only one architecture must be specified.', this);
10261027
}
10271028
this._architecture = props.architecture ?? (props.architectures && props.architectures[0]);
10281029

10291030
if (props.ephemeralStorageSize && !props.ephemeralStorageSize.isUnresolved()
10301031
&& (props.ephemeralStorageSize.toMebibytes() < 512 || props.ephemeralStorageSize.toMebibytes() > 10240)) {
1031-
throw new Error(`Ephemeral storage size must be between 512 and 10240 MB, received ${props.ephemeralStorageSize}.`);
1032+
throw new ValidationError(`Ephemeral storage size must be between 512 and 10240 MB, received ${props.ephemeralStorageSize}.`, this);
10321033
}
10331034

10341035
const resource: CfnFunction = new CfnFunction(this, 'Resource', {
@@ -1096,7 +1097,7 @@ export class Function extends FunctionBase {
10961097

10971098
if (props.layers) {
10981099
if (props.runtime === Runtime.FROM_IMAGE) {
1099-
throw new Error('Layers are not supported for container image functions');
1100+
throw new ValidationError('Layers are not supported for container image functions', this);
11001101
}
11011102

11021103
this.addLayers(...props.layers);
@@ -1109,7 +1110,7 @@ export class Function extends FunctionBase {
11091110
// Log retention
11101111
if (props.logRetention) {
11111112
if (props.logGroup) {
1112-
throw new Error('CDK does not support setting logRetention and logGroup');
1113+
throw new ValidationError('CDK does not support setting logRetention and logGroup', this);
11131114
}
11141115
const logRetention = new logs.LogRetention(this, 'LogRetention', {
11151116
logGroupName: `/aws/lambda/${this.functionName}`,
@@ -1137,7 +1138,7 @@ export class Function extends FunctionBase {
11371138

11381139
if (props.filesystem) {
11391140
if (!props.vpc) {
1140-
throw new Error('Cannot configure \'filesystem\' without configuring a VPC.');
1141+
throw new ValidationError('Cannot configure \'filesystem\' without configuring a VPC.', this);
11411142
}
11421143
const config = props.filesystem.config;
11431144
if (config.dependency) {
@@ -1201,7 +1202,7 @@ export class Function extends FunctionBase {
12011202
'LAMBDA_RUNTIME_DIR',
12021203
];
12031204
if (reservedEnvironmentVariables.includes(key)) {
1204-
throw new Error(`${key} environment variable is reserved by the lambda runtime and can not be set manually. See https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html`);
1205+
throw new ValidationError(`${key} environment variable is reserved by the lambda runtime and can not be set manually. See https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html`, this);
12051206
}
12061207
this.environment[key] = { value, ...options };
12071208
return this;
@@ -1214,24 +1215,24 @@ export class Function extends FunctionBase {
12141215
*/
12151216
private getLoggingConfig(props: FunctionProps): CfnFunction.LoggingConfigProperty | undefined {
12161217
if (props.logFormat && props.loggingFormat) {
1217-
throw new Error('Only define LogFormat or LoggingFormat, not both.');
1218+
throw new ValidationError('Only define LogFormat or LoggingFormat, not both.', this);
12181219
}
12191220

12201221
if (props.applicationLogLevel && props.applicationLogLevelV2) {
1221-
throw new Error('Only define applicationLogLevel or applicationLogLevelV2, not both.');
1222+
throw new ValidationError('Only define applicationLogLevel or applicationLogLevelV2, not both.', this);
12221223
}
12231224

12241225
if (props.systemLogLevel && props.systemLogLevelV2) {
1225-
throw new Error('Only define systemLogLevel or systemLogLevelV2, not both.');
1226+
throw new ValidationError('Only define systemLogLevel or systemLogLevelV2, not both.', this);
12261227
}
12271228

12281229
if (props.applicationLogLevel || props.applicationLogLevelV2 || props.systemLogLevel || props.systemLogLevelV2) {
12291230
if (props.logFormat !== LogFormat.JSON && props.loggingFormat === undefined) {
1230-
throw new Error(`To use ApplicationLogLevel and/or SystemLogLevel you must set LogFormat to '${LogFormat.JSON}', got '${props.logFormat}'.`);
1231+
throw new ValidationError(`To use ApplicationLogLevel and/or SystemLogLevel you must set LogFormat to '${LogFormat.JSON}', got '${props.logFormat}'.`, this);
12311232
}
12321233

12331234
if (props.loggingFormat !== LoggingFormat.JSON && props.logFormat === undefined) {
1234-
throw new Error(`To use ApplicationLogLevel and/or SystemLogLevel you must set LoggingFormat to '${LoggingFormat.JSON}', got '${props.loggingFormat}'.`);
1235+
throw new ValidationError(`To use ApplicationLogLevel and/or SystemLogLevel you must set LoggingFormat to '${LoggingFormat.JSON}', got '${props.loggingFormat}'.`, this);
12351236
}
12361237
}
12371238

@@ -1268,7 +1269,7 @@ export class Function extends FunctionBase {
12681269
*/
12691270
public invalidateVersionBasedOn(x: string) {
12701271
if (Token.isUnresolved(x)) {
1271-
throw new Error('invalidateVersionOn: input may not contain unresolved tokens');
1272+
throw new ValidationError('invalidateVersionOn: input may not contain unresolved tokens', this);
12721273
}
12731274
this.hashMixins.push(x);
12741275
}
@@ -1283,11 +1284,11 @@ export class Function extends FunctionBase {
12831284
public addLayers(...layers: ILayerVersion[]): void {
12841285
for (const layer of layers) {
12851286
if (this._layers.length === 5) {
1286-
throw new Error('Unable to add layer: this lambda function already uses 5 layers.');
1287+
throw new ValidationError('Unable to add layer: this lambda function already uses 5 layers.', this);
12871288
}
12881289
if (layer.compatibleRuntimes && !layer.compatibleRuntimes.find(runtime => runtime.runtimeEquals(this.runtime))) {
12891290
const runtimes = layer.compatibleRuntimes.map(runtime => runtime.name).join(', ');
1290-
throw new Error(`This lambda function uses a runtime that is incompatible with this layer (${this.runtime.name} is not in [${runtimes}])`);
1291+
throw new ValidationError(`This lambda function uses a runtime that is incompatible with this layer (${this.runtime.name} is not in [${runtimes}])`, this);
12911292
}
12921293

12931294
// Currently no validations for compatible architectures since Lambda service
@@ -1398,8 +1399,8 @@ export class Function extends FunctionBase {
13981399
}
13991400
const envKeys = Object.keys(this.environment);
14001401
if (envKeys.length !== 0) {
1401-
throw new Error(`The function ${this.node.path} contains environment variables [${envKeys}] and is not compatible with Lambda@Edge. \
1402-
Environment variables can be marked for removal when used in Lambda@Edge by setting the \'removeInEdge\' property in the \'addEnvironment()\' API.`);
1402+
throw new ValidationError(`The function ${this.node.path} contains environment variables [${envKeys}] and is not compatible with Lambda@Edge. \
1403+
Environment variables can be marked for removal when used in Lambda@Edge by setting the \'removeInEdge\' property in the \'addEnvironment()\' API.`, this);
14031404
}
14041405

14051406
return;
@@ -1435,19 +1436,19 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett
14351436
}
14361437

14371438
if (props.runtime === Runtime.FROM_IMAGE) {
1438-
throw new Error("ADOT Lambda layer can't be configured with container image package type");
1439+
throw new ValidationError("ADOT Lambda layer can't be configured with container image package type", this);
14391440
}
14401441

14411442
// This is not the complete list of incompatible runtimes and layer types. We are only
14421443
// checking for common mistakes on a best-effort basis.
14431444
if (this.runtime === Runtime.GO_1_X) {
1444-
throw new Error('Runtime go1.x is not supported by the ADOT Lambda Go SDK');
1445+
throw new ValidationError('Runtime go1.x is not supported by the ADOT Lambda Go SDK', this);
14451446
}
14461447

14471448
// The Runtime is Python and Adot is set it requires a different EXEC_WRAPPER than the other code bases.
14481449
if (this.runtime.family === RuntimeFamily.PYTHON &&
14491450
props.adotInstrumentation.execWrapper.valueOf() !== AdotLambdaExecWrapper.INSTRUMENT_HANDLER) {
1450-
throw new Error('Python Adot Lambda layer requires AdotLambdaExecWrapper.INSTRUMENT_HANDLER');
1451+
throw new ValidationError('Python Adot Lambda layer requires AdotLambdaExecWrapper.INSTRUMENT_HANDLER', this);
14511452
}
14521453

14531454
this.addLayers(LayerVersion.fromLayerVersionArn(this, 'AdotLayer', props.adotInstrumentation.layerVersion._bind(this).arn));
@@ -1510,47 +1511,47 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett
15101511
*/
15111512
private configureVpc(props: FunctionProps): CfnFunction.VpcConfigProperty | undefined {
15121513
if (props.securityGroup && props.securityGroups) {
1513-
throw new Error('Only one of the function props, securityGroup or securityGroups, is allowed');
1514+
throw new ValidationError('Only one of the function props, securityGroup or securityGroups, is allowed', this);
15141515
}
15151516

15161517
const hasSecurityGroups = props.securityGroups && props.securityGroups.length > 0;
15171518
if (!props.vpc) {
15181519
if (props.allowAllOutbound !== undefined) {
1519-
throw new Error('Cannot configure \'allowAllOutbound\' without configuring a VPC');
1520+
throw new ValidationError('Cannot configure \'allowAllOutbound\' without configuring a VPC', this);
15201521
}
15211522
if (props.securityGroup) {
1522-
throw new Error('Cannot configure \'securityGroup\' without configuring a VPC');
1523+
throw new ValidationError('Cannot configure \'securityGroup\' without configuring a VPC', this);
15231524
}
15241525
if (hasSecurityGroups) {
1525-
throw new Error('Cannot configure \'securityGroups\' without configuring a VPC');
1526+
throw new ValidationError('Cannot configure \'securityGroups\' without configuring a VPC', this);
15261527
}
15271528
if (props.vpcSubnets) {
1528-
throw new Error('Cannot configure \'vpcSubnets\' without configuring a VPC');
1529+
throw new ValidationError('Cannot configure \'vpcSubnets\' without configuring a VPC', this);
15291530
}
15301531
if (props.ipv6AllowedForDualStack) {
1531-
throw new Error('Cannot configure \'ipv6AllowedForDualStack\' without configuring a VPC');
1532+
throw new ValidationError('Cannot configure \'ipv6AllowedForDualStack\' without configuring a VPC', this);
15321533
}
15331534
if (props.allowAllIpv6Outbound !== undefined) {
1534-
throw new Error('Cannot configure \'allowAllIpv6Outbound\' without configuring a VPC');
1535+
throw new ValidationError('Cannot configure \'allowAllIpv6Outbound\' without configuring a VPC', this);
15351536
}
15361537
return undefined;
15371538
}
15381539

15391540
if (props.allowAllOutbound !== undefined) {
15401541
if (props.securityGroup) {
1541-
throw new Error('Configure \'allowAllOutbound\' directly on the supplied SecurityGroup.');
1542+
throw new ValidationError('Configure \'allowAllOutbound\' directly on the supplied SecurityGroup.', this);
15421543
}
15431544
if (hasSecurityGroups) {
1544-
throw new Error('Configure \'allowAllOutbound\' directly on the supplied SecurityGroups.');
1545+
throw new ValidationError('Configure \'allowAllOutbound\' directly on the supplied SecurityGroups.', this);
15451546
}
15461547
}
15471548

15481549
if (props.allowAllIpv6Outbound !== undefined) {
15491550
if (props.securityGroup) {
1550-
throw new Error('Configure \'allowAllIpv6Outbound\' directly on the supplied SecurityGroup.');
1551+
throw new ValidationError('Configure \'allowAllIpv6Outbound\' directly on the supplied SecurityGroup.', this);
15511552
}
15521553
if (hasSecurityGroups) {
1553-
throw new Error('Configure \'allowAllIpv6Outbound\' directly on the supplied SecurityGroups.');
1554+
throw new ValidationError('Configure \'allowAllIpv6Outbound\' directly on the supplied SecurityGroups.', this);
15541555
}
15551556
}
15561557

@@ -1585,8 +1586,8 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett
15851586
const publicSubnetIds = new Set(props.vpc.publicSubnets.map(s => s.subnetId));
15861587
for (const subnetId of selectedSubnets.subnetIds) {
15871588
if (publicSubnetIds.has(subnetId) && !allowPublicSubnet) {
1588-
throw new Error('Lambda Functions in a public subnet can NOT access the internet. ' +
1589-
'If you are aware of this limitation and would still like to place the function in a public subnet, set `allowPublicSubnet` to true');
1589+
throw new ValidationError('Lambda Functions in a public subnet can NOT access the internet. ' +
1590+
'If you are aware of this limitation and would still like to place the function in a public subnet, set `allowPublicSubnet` to true', this);
15901591
}
15911592
}
15921593
this.node.addDependency(selectedSubnets.internetConnectivityEstablished);
@@ -1622,15 +1623,15 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett
16221623
Annotations.of(this).addWarningV2('@aws-cdk/aws-lambda:snapStartRequirePublish', 'SnapStart only support published Lambda versions. Ignore if function already have published versions');
16231624

16241625
if (!props.runtime.supportsSnapStart) {
1625-
throw new Error(`SnapStart currently not supported by runtime ${props.runtime.name}`);
1626+
throw new ValidationError(`SnapStart currently not supported by runtime ${props.runtime.name}`, this);
16261627
}
16271628

16281629
if (props.filesystem) {
1629-
throw new Error('SnapStart is currently not supported using EFS');
1630+
throw new ValidationError('SnapStart is currently not supported using EFS', this);
16301631
}
16311632

16321633
if (props.ephemeralStorageSize && props.ephemeralStorageSize?.toMebibytes() > 512) {
1633-
throw new Error('SnapStart is currently not supported using more than 512 MiB Ephemeral Storage');
1634+
throw new ValidationError('SnapStart is currently not supported using more than 512 MiB Ephemeral Storage', this);
16341635
}
16351636

16361637
return props.snapStart._render();
@@ -1648,7 +1649,7 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett
16481649
throw Error('deadLetterQueue defined but deadLetterQueueEnabled explicitly set to false');
16491650
}
16501651
if (props.deadLetterTopic && (props.deadLetterQueue || props.deadLetterQueueEnabled !== undefined)) {
1651-
throw new Error('deadLetterQueue and deadLetterTopic cannot be specified together at the same time');
1652+
throw new ValidationError('deadLetterQueue and deadLetterTopic cannot be specified together at the same time', this);
16521653
}
16531654

16541655
let deadLetterQueue: sqs.IQueue | sns.ITopic;
@@ -1698,7 +1699,7 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett
16981699

16991700
private validateProfiling(props: FunctionProps) {
17001701
if (!props.runtime.supportsCodeGuruProfiling) {
1701-
throw new Error(`CodeGuru profiling is not supported by runtime ${props.runtime.name}`);
1702+
throw new ValidationError(`CodeGuru profiling is not supported by runtime ${props.runtime.name}`, this);
17021703
}
17031704
if (props.environment && (props.environment.AWS_CODEGURU_PROFILER_GROUP_NAME
17041705
|| props.environment.AWS_CODEGURU_PROFILER_GROUP_ARN

0 commit comments

Comments
 (0)