6

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)

3
  • How did you manage to use YAML to define your state machines? Commented May 26, 2018 at 2:03
  • 1
    That’s the default when you’re using the “serverless-step-functions” plugin. Commented May 26, 2018 at 2:06
  • I’ve recently changed my build tooling so I can configure in Typescript and get the benefits of strong typing. Makes life much easier. Commented May 26, 2018 at 2:09

2 Answers 2

7

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)\"]}"
}
Sign up to request clarification or add additional context in comments.

1 Comment

interesting. could have sworn i tried this and it didn’t work for me. i’ll have another look in the morning. thanks.
3

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.

2 Comments

I do use the callback to return the error. Also, I think in my explanation above I suggest that I tried both approaches you suggest here; am I missing something?
The important thing was callback call (I inserted code snippets to show that both approaches work). I do not see anywhere in your code a call to callback. If an exception is not catched and returned with callback then you will always get an Error in step function (that's why I pointed out that as an issue). You can run CloudFormation template that I provided (it has no dependencies) - it will be a stable base for finding out where the issue lays (still I suspect some problem with catching exception and returning it with callback).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.