Skip to content

RFC: Better Error Handling #206

@cesarParra

Description

@cesarParra

Currently, errors are not handled by default by Expression. This can lead to needing code with a lot of verbosity to check all potential nulls/blanks to avoid the errors from happening. Ideally, there is a simpler way that is built-into Expression so that users can handle errors without all of the extra verbosity.

Here are some some different approaches/alternatives (I haven't had a chance to prototype any of these yet).

Potential Approaches

Approach 1: Introducing a TRY function

Proposed syntax

TRY(
  SOMETHING_THAT_MIGHT_FAIL()
)

# Or alternatively

DO_SOMETHING()
-> DO_SOMETHING_ELSE()
-> ETC()
-> TRY() # a single try at the end of the chain to catch everything

This function would return either the Object with the result or a new special Error object with information about the failure. Another function can then be introduced to check if the return value is of this special Error, allowing users to recover from the errors

TRY(
  DO_SOMETHING()
)
-> IS_ERROR(fallbackValue) # if not an error, return the value, but if an error was encountered return a fallback value

Alternatively, these 2 things could be combined into a single function that returns the fallback if the main operation fails. For example:

TRY_OR_ELSE(
  SOMETHING_DANGEROUS(),
  FALLBACK_EXPRESSION
)

# Or with pipes

SOMETHING_DANGEROUS()
-> SOMETHING_ELSE()
-> TRY_OR_ELSE(FALLBACK)

Advantages

The advantage of this how much control the user would have. TRY could be used to wrap the entire expression, or it can be used to wrap a very specific thing that might fail and recover from just that junction and continue with the rest. For example:

DO_SOMETHING_SAFE()
-> TRY_OR_ELSE(SOMETHING_DANGEROUS(), FALLBACK_VALUE)
-> KEEP_GOING_WITHOUT_FAILURES()

Disadvantages

This puts the responsibility of error handling on the hands of each user for each expression they write. Instead of having a global way of everything being safe, for each expression users will need to remember to wrap it in a try if they are dangerous.


Approach 2: Global catching

This approach would allow API users to suppress all errors at the API level, rather than having to handle each expression.

Proposed Solution

This PR #205 by @AlecCollins serves as an example for this approach.

Advantages

Takes the responsibility away from each expression writer and introduces a fix-all for every expression evaluated.

Disadvantages

  • Takes away error recovery from the expression writer
  • Can hide away the errors that occur and the type of error, making debugging harder (see the Additional Considerations section for the different types of errors)

One other thing to consider about this approach is that today API users can achieve this without any internal changes to the Expression codebase, by wrapping the Expression API:

public class CustomEvaluator {
  public static Object evaluate(String expression) {
    try {
      expression.Evaluator.run(expression)
    } catch (Exception e)
      // do something or return fallback
    }
  }
}

Approach 3: Result types for everything that can fail

A lot of modern languages have the concept of a type that represent errors (usually called Result, sometimes called Either). For any internal function that can fail we can, instead of letting it fail, change their return type to be a Result, with either the correct value or an error string, which can be used to debug exactly which operation failed and why.

This would give us very similar advantages to approach #1, since each function would be able to provide recoverability and traceability.

Advantages

Disadvantages

  • Biggest amount of implementation effort
  • Might require breaking changes, but there is probably a version of the implementation that can be done without breaking any existing expressions

Additional Considerations

"Errors" can mean a lot of things. For instance, here is a non-exhaustive list of the errors thrown by expression:

  • Syntax error. When there is an error on the expression itself. Internally, Expression throws a ParsingException
  • Rutime error. When the expression is valid, but there an error occurred when running it
  • Environment error. When the code is trying to access variables that are not defined in the given context
  • Context resolution error. When the automated context resolver fails to convert the expression into valid SObject relationships to query
  • ActionVariable errors. When incorectly using ${FOO_GLOBAL_VARIABLES}
  • Custom function errors. When an error occurs inside of a custom function created by the user.

A lot of these errors should be handled in different ways. For example, syntax errors should probably not be caught, because there is a fundamental error with the expression itself, which means it will never run successfully.

Errors in custom function should probably not be caught either, since this is custom code written by a developer. Instead, I believe that the Expression API should provide something (I don't know yet what, probably the special Error type defined in approach #1), so that developers writing their custom functions can either return a correct value or an error representation that the Expression interpreter can understand and recover from, giving flexibility to the function implementor for them to define how to recover from errors - e.g. fallback, let it fail, etc..

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions