I have an AWS step-function/state-machine of Lambda functions written primarily in Javascript (although one is in Java) and I'd like to manage the error processing better.
I have no problem with having an error condition being caught and then forwarded to another state in the flow. So for instance, the following state definition in my state machine passes execution to the NotifyOfError
state where I am able to email and sms appropriately about the error state.
Closure:
Type: Task
Resource: >-
arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:xxx-services-${opt:stage}-transportClosure
Next: WaitForCloudWatch
Catch:
- ErrorEquals:
- "States.ALL"
ResultPath: "$.error-info"
Next: NotifyOfError
However, rather than hand ALL errors to this one state there are a few errors I'd like handle differently. So at first I thought that if I threw a Javascript/Node error with a given "name" then that name would be something I could branch off of in the ErrorEquals configuration. Example:
catch(e) {
if (e.message.indexOf('something') !== -1) {
e.name = "SomethingError";
throw e;
}
but soon realized that name was only being prepended to the Cause
portion of the step-function and not something that would branch. I then tried extending the base Error class like so:
export default class UndefinedAssignment extends Error {
constructor(e: Error) {
super(e.message);
this.stack = e.stack;
}
}
but throwing this error actually did nothing, meaning that by the time it showed up in the Step Function the Error's type was still just "Error":
"error-info": {
"Error": "Error",
"Cause": "{\"errorMessage\":\"Error: the message",\"errorType\":\"Error\",\"stackTrace\":[\"db.set.catch.e (/var/task/lib/prepWorker/Handler.js:247:23)\",\"process._tickDomainCallback (internal/process/next_tick.js:135:7)\"]}"
}
So I'm still unclear how I can distinguish errors sourced in Node that are branchable within the step function.
Note: with Java, it appears it does pickup the error class correctly (although I've done far less testing on the Java side)
Here's how I get Step Functions to report a custom error and message as its Error
and Cause
. Note I'm using the Node.js 8.10 Lambda runtime with async
and try/catch
.
exports.handler = async (event) => {
function GenericError(name, message) {
this.name = name;
this.message = message;
}
GenericError.prototype = new Error();
try {
// my implementation which might throw an error
// ...
}
catch (e) {
console.log(e);
let error = new GenericError('CustomError', 'my message');
throw error;
}
};
Note for simplicity I'm ignoring the error object from catch(e)
here. You could also feed its stack
into the GenericError if wanted.
This lambda function returns:
{
"errorMessage": "my message",
"errorType": "CustomError",
"stackTrace": [
"exports.handler (/var/task/index.js:33:28)"
]
}
Step Functions turns this into:
{
"error": "CustomError",
"cause": {
"errorMessage": "my message",
"errorType": "CustomError",
"stackTrace": [
"exports.handler (/var/task/index.js:33:28)"
]
}
}
in its LambdaFunctionFailed
event history, and ultimately converts it again into this state output (depending on our ResultPath
- here without any):
{
"Error": "CustomError",
"Cause": "{\"errorMessage\":\"my message\",\"errorType\":\"CustomError\",\"stackTrace\":[\"exports.handler (/var/task/index.js:33:28)\"]}"
}
You should return thrown exception from Lambda using callback
. Example Cloud Formation template creating both lambda and state machine:
AWSTemplateFormatVersion: 2010-09-09
Description: Stack creating AWS Step Functions state machine and lambda function throwing custom error.
Resources:
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
Handler: "index.handler"
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile: |
exports.handler = function(event, context, callback) {
function SomethingError(message) {
this.name = "SomethingError";
this.message = message;
}
SomethingError.prototype = new Error();
const error = new SomethingError("something-error");
callback(error);
};
Runtime: "nodejs6.10"
Timeout: 25
StateMachine:
Type: AWS::StepFunctions::StateMachine
Properties:
RoleArn: !GetAtt StatesExecutionRole.Arn
DefinitionString: !Sub
- >
{
"Comment": "State machine for nodejs error handling experiment",
"StartAt": "FirstState",
"States": {
"FirstState": {
"Type": "Task",
"Resource": "${ThrowErrorResource}",
"Next": "Success",
"Catch": [
{
"ErrorEquals": ["SomethingError"],
"ResultPath": "$.error",
"Next": "CatchSomethingError"
}
]
},
"Success": {
"Type": "Pass",
"End": true
},
"CatchSomethingError": {
"Type": "Pass",
"Result": {
"errorHandlerOutput": "Huh, I catched an error"
},
"ResultPath": "$.errorHandler",
"End": true
}
}
}
- ThrowErrorResource: !GetAtt LambdaFunction.Arn
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
StatesExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- !Sub states.${AWS::Region}.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: ExecuteLambda
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: arn:aws:lambda:*:*:function:*
Essential part is Lambda Function definition:
exports.handler = function(event, context, callback) {
function SomethingError(message) {
this.name = "SomethingError";
this.message = message;
}
SomethingError.prototype = new Error();
const error = new SomethingError("something-error");
callback(error);
};
Custom error with custom name is defined here. Of course you can also simply overwrite name (but I do not recommend that):
exports.handler = function(event, context, callback) {
var e = new Error();
e.name = "SomethingError";
callback(e);
};
Error returned like that will be passed to Step Functions without losing error name. I suggest creating some top try-catch
statement in Lambda Function where you would simply call callback
with error.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With