Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Managing error flow in AWS step-functions

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)

like image 653
ken Avatar asked Mar 19 '18 21:03

ken


2 Answers

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)\"]}"
}
like image 166
kino1 Avatar answered Sep 23 '22 16:09

kino1


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.

like image 40
Marcin Sucharski Avatar answered Sep 24 '22 16:09

Marcin Sucharski