Skip to main content

Error Handling

Expresso provides robust error handling mechanisms to help you manage errors in your expressions.

Exception Hierarchy

Expresso uses a comprehensive exception hierarchy to provide clear and informative error messages. All exceptions extend from the base ExpressionException class.

ExpressionException
├── SyntaxException // For parsing and syntax errors
├── EvaluationException // For errors during expression evaluation
│ ├── FunctionExecutionException // For errors in function execution
│ │ └── MissingArgumentException // When function arguments are missing
│ ├── TypeConversionException // For type conversion errors
│ ├── InvalidOperationException // For invalid operations between operands
│ │ └── ArithmeticExpressionException // For arithmetic errors like division by zero
│ └── UnknownFunctionException // When a referenced function doesn't exist
└── PropertyNotFoundException // For property access errors
├── VariableNotFoundException // When a variable doesn't exist
├── PropertyAccessException // When a property exists but can't be accessed
└── ArrayIndexOutOfBoundsException // When an array index is out of bounds

Understanding this hierarchy helps you catch specific types of errors and handle them appropriately.

Syntax Errors

The SyntaxException is thrown when there's a syntax error in the expression, such as missing parentheses or invalid operators.

try {
evaluator.evaluate("5 + ", context);
} catch (SyntaxException e) {
System.err.println("Syntax error: " + e.getMessage());
// e.g. "Unexpected end of expression"
}

Common syntax errors include:

  • Unclosed parentheses or string literals
  • Missing operators between values
  • Invalid array indices
  • Malformed expressions

Evaluation Errors

The EvaluationException is the base class for all errors that occur during expression evaluation.

Function Execution Errors

  • FunctionExecutionException: Thrown when an error occurs within a function call
  • MissingArgumentException: Thrown when a function is called with fewer arguments than expected
  • UnknownFunctionException: Thrown when referencing a function that doesn't exist
try {
evaluator.evaluate("pow(2)", context); // Missing second argument
} catch (MissingArgumentException e) {
System.err.println("Missing argument: " + e.getMessage());
System.err.println("Function: " + e.getFunctionName());
System.err.println("Expected: " + e.getExpectedCount() + " arguments");
System.err.println("Actual: " + e.getActualCount() + " arguments");
}

try {
evaluator.evaluate("unknownFunction()", context);
} catch (UnknownFunctionException e) {
System.err.println("Unknown function: " + e.getFunctionName());
}

try {
evaluator.evaluate("upperCase(42)", context); // Passing a number to a string function
} catch (FunctionExecutionException e) {
System.err.println("Function error: " + e.getMessage());
System.err.println("Function name: " + e.getFunctionName());
}

Type Conversion Errors

The TypeConversionException is thrown when a value cannot be converted to the expected type.

try {
evaluator.evaluate("sqrt('hello')", context); // Cannot convert string to number
} catch (TypeConversionException e) {
System.err.println("Type conversion error: " + e.getMessage());
System.err.println("Source value: " + e.getSourceValue());
System.err.println("Target type: " + e.getTargetType());
}

Operation Errors

  • InvalidOperationException: Thrown when an operation is invalid between two operands
  • ArithmeticExpressionException: Specifically for arithmetic errors like division by zero
try {
evaluator.evaluate("'hello' * 5", context); // Cannot multiply string by number
} catch (InvalidOperationException e) {
System.err.println("Invalid operation: " + e.getMessage());
System.err.println("Operation: " + e.getOperation());
System.err.println("Left operand: " + e.getLeftOperand());
System.err.println("Right operand: " + e.getRightOperand());
}

try {
evaluator.evaluate("5 / 0", context); // Division by zero
} catch (ArithmeticExpressionException e) {
System.err.println("Arithmetic error: " + e.getMessage());
// e.g. "Invalid operation '/' between 5 and 0: Division by zero"
}

Property Access Errors

The PropertyNotFoundException is the base class for errors related to accessing properties that don't exist.

  • VariableNotFoundException: Thrown when referencing a variable that doesn't exist in the context
  • PropertyAccessException: Thrown when a property exists but cannot be accessed
  • ArrayIndexOutOfBoundsException: Thrown when an array or list index is out of bounds
try {
evaluator.evaluate("$nonExistentVar", context);
} catch (VariableNotFoundException e) {
System.err.println("Variable not found: " + e.getVariableName());
}

try {
evaluator.evaluate("$person.address.city", context); // When address is null
} catch (PropertyAccessException e) {
System.err.println("Property access error: " + e.getMessage());
System.err.println("Target object: " + e.getTarget());
System.err.println("Property name: " + e.getProperty());
}

try {
evaluator.evaluate("$scores[5]", context); // When scores has fewer than 6 elements
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println("Array index error: " + e.getMessage());
System.err.println("Array: " + e.getArray());
System.err.println("Index: " + e.getIndex());
System.err.println("Size: " + e.getSize());
}

Avoiding Errors with Null-Safe Operators

Expresso provides null-safe operators to help you avoid PropertyNotFoundException exceptions when dealing with potentially null values:

// This will throw PropertyNotFoundException if person.address is null
try {
evaluator.evaluate("$person.address.city", context);
} catch (PropertyNotFoundException e) {
System.err.println("Property error: " + e.getMessage());
// e.g. "Cannot access property on null value"
}

// Using null-safe property access - returns null instead of throwing exception
Object result = evaluator.evaluate("$person?.address?.city", context);
// result will be null if person or address is null

// Using null coalescing for default values
String city = (String) evaluator.evaluate("$person?.address?.city ?? 'Unknown'", context);
// city will be "Unknown" if person or address is null

// Safer array access
try {
evaluator.evaluate("$person.hobbies[0]", context); // Throws if hobbies is null
} catch (PropertyNotFoundException e) {
System.err.println("Array error: " + e.getMessage());
}

// Using null-safe array access
Object hobby = evaluator.evaluate("$person?.hobbies?[0]", context);
// hobby will be null if hobbies is null or empty

// Combining null-safe array access with null coalescing
String hobby = (String) evaluator.evaluate("$person?.hobbies?[0] ?? 'No hobby'", context);
// hobby will be "No hobby" if hobbies is null or the index is out of bounds

Defensive Functions

Expresso provides several built-in functions that can help with error prevention:

  • isNull(value): Checks if a value is null
  • isEmpty(value): Checks if a string, array, or collection is empty
  • coalesce(value1, value2, ...): Returns the first non-null value in the list of arguments
// Using coalesce to handle potentially null values
Object result = evaluator.evaluate("coalesce($possiblyNullVar, $backupVar, 'default')", context);

// Checking if a value is null before using it
Object result = evaluator.evaluate("$var == null ? 'Value is null' : 'Value is: ' + $var", context);

// Checking if a list is empty before accessing elements
Object result = evaluator.evaluate("isEmpty($list) ? 'List is empty' : 'First item: ' + $list[0]", context);

Error Handling Best Practices

  1. Catch Specific Exceptions First: Always catch the most specific exceptions first, followed by more general ones.
try {
evaluator.evaluate(expression, context);
} catch (SyntaxException e) {
// Handle syntax errors
} catch (MissingArgumentException e) {
// Handle missing arguments in functions
} catch (UnknownFunctionException e) {
// Handle unknown functions
} catch (FunctionExecutionException e) {
// Handle other function execution errors
} catch (TypeConversionException e) {
// Handle type conversion errors
} catch (ArithmeticExpressionException e) {
// Handle arithmetic errors
} catch (InvalidOperationException e) {
// Handle other operation errors
} catch (ArrayIndexOutOfBoundsException e) {
// Handle array index errors
} catch (VariableNotFoundException e) {
// Handle variable not found errors
} catch (PropertyAccessException e) {
// Handle property access errors
} catch (PropertyNotFoundException e) {
// Handle other property errors
} catch (EvaluationException e) {
// Handle other evaluation errors
} catch (ExpressionException e) {
// Handle any other expression errors
} catch (Exception e) {
// Handle unexpected errors
}
  1. Use Exception Properties: Many exceptions provide additional information through methods like getFunctionName(), getVariableName(), etc.

  2. User-Friendly Error Messages: Translate exceptions into user-friendly error messages that explain how to fix the issue.

  3. Default Values: Consider using null-safe operators (?., ?[]) and null coalescing (??) to handle potential errors gracefully.

  4. Validate Expressions: For critical code paths, validate expressions before evaluating them to prevent runtime errors.

  5. Properly Type-Check: Ensure proper type checking in custom functions to avoid type conversion errors.

  6. Use Defensive Functions: Leverage built-in functions like isNull(), isEmpty(), and coalesce() to handle edge cases.

Using these techniques, you can create expressions that gracefully handle errors and provide a better user experience.