Error Handling Patterns
This guide provides comprehensive coverage of error handling in Constellation Engine, including compiler diagnostics, runtime errors, recovery strategies, and testing patterns for LLM-assisted development.
Overview
Constellation Engine has a sophisticated error handling system that spans three main phases:
- Parse-time errors — Syntax and structure validation
- Compile-time errors — Type checking, semantic analysis, and DAG construction
- Runtime errors — Module execution, lifecycle, and resource management
Each phase produces structured, actionable error messages with precise location information, suggestions, and documentation links.
Error Hierarchy
Compile-Time Errors (CompileError)
All compile-time errors extend the CompileError sealed trait from the AST module:
sealed trait CompileError {
def message: String
def span: Option[Span] // Source location (start/end position)
}
Error Categories:
| Category | Error Codes | Description |
|---|---|---|
| Reference | E001-E009 | Undefined variables, functions, types, namespaces |
| Type | E010-E019 | Type mismatches, incompatible operations |
| Syntax | E020-E029 | Parse errors, unexpected tokens |
| Semantic | E030-E039 | Duplicate definitions, circular dependencies |
| Internal | E900+ | Compiler bugs (should be reported) |
Runtime Errors
Runtime errors use different hierarchies depending on the subsystem:
// Execution errors (from errors package)
sealed trait ApiError {
def message: String
}
// Lifecycle exceptions
class CircuitOpenException(moduleName: String)
class QueueFullException(maxSize: Int)
class ShutdownRejectedException()
// Suspendable execution errors
case class InputTypeMismatchError(name: String, expected: CType, actual: CType)
case class InputAlreadyProvidedError(name: String)
case class UnknownNodeError(name: String)
DAG Compilation Errors (CompilerError)
Lower-level IR compilation errors:
sealed trait CompilerError {
def message: String
}
// Common variants:
case class NodeNotFound(nodeId: UUID, context: String)
case class LambdaParameterNotBound(paramName: String)
case class UnsupportedOperation(operation: String)
case class InvalidFieldAccess(field: String, actualType: String)
Common Error Types
1. Parse Errors (E020-E029)
Parse errors occur when the source code has invalid syntax.
E020: Syntax Error
Cause: Invalid syntax that the parser cannot understand.
Examples:
# Missing closing parenthesis
result = Uppercase(text
out result
Error:
Error E020: Syntax error
--> line 1, column 22
1 │ result = Uppercase(text
│ ^
2 │ out result
Parse error: expected ')' but got end of line
The parser encountered invalid syntax.
Check for:
- Missing or extra parentheses
- Missing commas between arguments
- Typos in keywords
- Unclosed strings or brackets
→ Check for unclosed parentheses or brackets
→ Verify all function calls have matching '(' and ')'
Recovery Strategy:
- Check the indicated line and column
- Look at the previous line for unclosed delimiters
- Verify matching parentheses/brackets/braces
- Check for typos in keywords (
in,out,type,use)
Common Mistakes:
# WRONG: Missing type annotation
in x
out x
# CORRECT:
in x: String
out x
# WRONG: Missing comma in function args
result = Add(x y)
# CORRECT:
result = Add(x, y)
# WRONG: Unclosed string literal
message = "Hello
out message
# CORRECT:
message = "Hello"
out message
# WRONG: Invalid identifier (starts with digit)
in 123value: Int
# CORRECT:
in value123: Int
E021: Unexpected Token
Cause: Parser found a token it didn't expect at this position.
Example:
in x: Int
out @ result
Error:
Error E021: Unexpected token
--> line 2, column 5
2 │ out @ result
│ ^
The parser found a token it didn't expect at this position.
This usually indicates a syntax error nearby.
→ Remove the unexpected '@' character
→ Valid output syntax: out identifier
Recovery Strategy:
- Remove invalid characters
- Check if you're using reserved operators in wrong contexts
- Verify identifier naming rules (alphanumeric + underscore, can't start with digit)
2. Reference Errors (E001-E009)
Reference errors occur when you use a name that doesn't exist.
E001: Undefined Variable
Cause: Using a variable that hasn't been declared or assigned.
Example:
in text: String
result = Uppercase(textt) # Typo: 'textt' instead of 'text'
out result
Error:
Error E001: Undefined variable
--> line 2, column 20
1 │ in text: String
2 │ result = Uppercase(textt)
│ ^^^^^
3 │ out result
Undefined variable: textt
The variable you're trying to use has not been declared.
Variables must be declared before use:
- As an input: in variableName: Type
- As an assignment: variableName = SomeModule(...)
→ Did you mean 'text'?
See: https://constellation-engine.dev/docs/constellation-lang/declarations
Recovery Strategy:
- Check for typos — the error includes "Did you mean?" suggestions
- Verify the variable is declared before use (order matters)
- Check variable name is case-sensitive
- If truly undefined, add an input declaration:
in variableName: Type
Common Mistakes:
# WRONG: Using before declaring
out result
result = Compute(x)
# CORRECT: Declare/assign before use
result = Compute(x)
out result
# WRONG: Case mismatch
in userName: String
result = Process(username) # 'username' != 'userName'
# CORRECT:
in userName: String
result = Process(userName)
# WRONG: Referencing non-existent field
in data: { id: Int, name: String }
email = data.email # 'email' field doesn't exist
# CORRECT:
in data: { id: Int, name: String, email: String }
email = data.email
E002: Undefined Function
Cause: Calling a function that isn't registered with the compiler.
Example:
in text: String
result = Upppercase(text) # Typo: 'Upppercase' instead of 'Uppercase'
out result
Error:
Error E002: Undefined function
--> line 2, column 10
2 │ result = Upppercase(text)
│ ^^^^^^^^^^
Undefined function: Upppercase
The function you're trying to call is not registered.
Make sure the function is:
- Spelled correctly (function names are case-sensitive)
- Registered with the compiler via StdLib or custom modules
- Imported if it's from a namespace: use stdlib.math
→ Did you mean 'Uppercase'?
See: https://constellation-engine.dev/docs/constellation-lang/functions
Recovery Strategy:
- Check for typos in function name
- Verify the module is registered in your Scala code:
constellation.setModule(myModule) - Check the
ModuleBuilder.metadata()name matches exactly:ModuleBuilder.metadata("Uppercase", ...) // Must match usage - For namespaced functions, add
usedeclaration:use stdlib.math
result = math.add(x, y)
Common Mistakes:
# WRONG: Function not registered
result = CustomFunction(data)
# Need to register in Scala: constellation.setModule(customFunction)
# WRONG: Case mismatch
result = uppercase(text) # Should be 'Uppercase'
# CORRECT:
result = Uppercase(text)
# WRONG: Missing namespace
result = add(1, 2) # 'add' is in stdlib.math
# CORRECT:
use stdlib.math
result = math.add(1, 2)
E003: Undefined Type
Cause: Using a type name that isn't defined.
Example:
in data: MyCustomType # MyCustomType not defined
out data
Error:
Error E003: Undefined type
--> line 1, column 10
1 │ in data: MyCustomType
│ ^^^^^^^^^^^^
Undefined type: MyCustomType
The type you specified is not defined.
Built-in types: String, Int, Float, Boolean
Collections: List<T>, Map<K, V>, Optional<T>
Custom types must be declared: type MyType = { field: Type }
→ Define the type first: type MyCustomType = { ... }
See: https://constellation-engine.dev/docs/constellation-lang/types
Recovery Strategy:
- Check for typos in type name
- Verify built-in types are spelled correctly (case-sensitive)
- For custom types, add a type definition:
type MyCustomType = { id: Int, name: String } - For parameterized types, use correct syntax:
List<T>, notList[T]
Common Mistakes:
# WRONG: Typo in built-in type
in count: Integer # Should be 'Int'
# CORRECT:
in count: Int
# WRONG: Using undefined custom type
in user: User
# CORRECT: Define type first
type User = { id: Int, name: String }
in user: User
# WRONG: Wrong bracket syntax
in items: List[String] # Should use '<>'
# CORRECT:
in items: List<String>
E006: Invalid Projection
Cause: Trying to project a field that doesn't exist on a record type.
Example:
in data: { id: Int, name: String }
result = data[id, email] # 'email' doesn't exist
out result
Error:
Error E006: Invalid projection
--> line 2, column 19
2 │ result = data[id, email]
│ ^^^^^
Invalid projection: field 'email' not found
The field you're trying to project doesn't exist on this type.
Projection syntax: record[field1, field2]
This creates a new record with only the specified fields.
→ Did you mean 'name'?
→ Available fields: id, name
See: https://constellation-engine.dev/docs/constellation-lang/expressions
Recovery Strategy:
- Check field name spelling
- Verify the field exists in the record type definition
- Use hover info in VSCode to see available fields
- Consider adding the field to the type definition if needed
E007: Invalid Field Access
Cause: Trying to access a field that doesn't exist using dot notation.
Example:
in user: { id: Int, name: String }
email = user.email # 'email' field doesn't exist
out email
Error:
Error E007: Invalid field access
--> line 2, column 14
2 │ email = user.email
│ ^^^^^
Invalid field access: field 'email' not found
The field you're trying to access doesn't exist on this type.
Field access syntax: record.fieldName
The source expression must be a record type with that field.
→ Available fields: id, name
See: https://constellation-engine.dev/docs/constellation-lang/expressions
Recovery Strategy:
- Check available fields (listed in error message)
- Verify the upstream type definition
- Use projection to select subset of fields:
user[id, name]
3. Type Errors (E010-E019)
Type errors occur when types don't match expectations.
E010: Type Mismatch
Cause: The actual type doesn't match the expected type.
Example:
in count: Int
result = Uppercase(count) # Uppercase expects String, got Int
out result
Error:
Error E010: Type mismatch
--> line 2, column 20
2 │ result = Uppercase(count)
│ ^^^^^
Type mismatch: expected String, got Int
The actual type does not match the expected type.
This often happens when:
- Passing wrong argument type to a function
- Assigning incompatible value to a variable
- Returning wrong type from a conditional
→ Use ToString(count) to convert to String
See: https://constellation-engine.dev/docs/constellation-lang/type-system
Recovery Strategy:
- Check function signature to see what types are expected
- Use type conversion functions:
ToString(x)— Convert any value to StringToInt(x)— Convert Float to Int (truncates)- Int automatically promotes to Float in arithmetic
- Verify your input declarations match the data you're providing
- Use hover in VSCode to see inferred types
Common Type Conversions:
# String conversions
in count: Int
message = ToString(count) # Int -> String
# Numeric conversions
in price: Float
dollars = ToInt(price) # Float -> Int (truncates)
in quantity: Int
total = quantity * 1.5 # Int -> Float (automatic promotion)
# Optional handling
in maybeValue: Optional<Int>
value = maybeValue ?? 0 # Extract with default
# Boolean from comparison
in age: Int
isAdult = age >= 18 # Int -> Boolean via comparison
E012: Incompatible Merge
Cause: Trying to merge types that can't be merged with the + operator.
Example:
in count: Int
in text: String
result = count + text # Can't merge Int and String
out result
Error:
Error E012: Incompatible types for merge
--> line 3, column 10
3 │ result = count + text
│ ^^^^^^^^^^^^
Cannot merge types: Int + String
Cannot merge these types with the + operator.
The merge operator requires compatible types:
- Two records (fields are merged)
- Two Candidates (element-wise merge)
- Candidates + Record (broadcast)
- Record + Candidates (broadcast)
→ For numeric addition, use stdlib.math.add(count, ...)
→ The + operator is for merging records, not arithmetic
See: https://constellation-engine.dev/docs/constellation-lang/operators
Recovery Strategy:
- Important: In constellation-lang,
+is for record/candidates merge, NOT arithmetic - For numeric addition, use
stdlib.math.add(a, b) - For record merge, both sides must be records:
in a: { x: Int }
in b: { y: String }
merged = a + b # Valid: creates { x: Int, y: String } - For candidates merge, both must be Candidates or one can be a record:
type A = { x: Int }
type B = { y: String }
in items1: Candidates<A>
in items2: Candidates<B>
merged = items1 + items2 # Element-wise merge
Common Mistakes:
# WRONG: Using + for arithmetic
in a: Int
in b: Int
sum = a + b # + is for merging, not addition!
# CORRECT: Use stdlib.math
use stdlib.math
in a: Int
in b: Int
sum = math.add(a, b)
# WRONG: Merging incompatible types
in count: Int
in data: { name: String }
result = count + data # Int can't merge with record
# CORRECT: Merge compatible records
in data1: { x: Int }
in data2: { y: String }
result = data1 + data2 # Both are records
E013: Unsupported Comparison
Cause: Using a comparison operator with unsupported types.
Example:
in a: { x: Int }
in b: { y: String }
result = a == b # Can't compare different record types
out result
Recovery Strategy:
- Ensure both operands have compatible types
- Comparison operators work with:
- Int and Int
- Float and Float
- String and String (lexicographic)
- Boolean and Boolean (== and != only)
- For complex types, compare specific fields:
match = (a.x == b.x) and (a.y == b.y)
E016: Invalid Option Value
Cause: Providing an invalid value for a module call option.
Example:
in data: String
result = Process(data) with retry: -1 # Negative retry count
out result
Error:
Error E016: Invalid option value
--> line 2, column 36
2 │ result = Process(data) with retry: -1
│ ^^
Invalid option value: retry must be >= 0
The value provided for a module call option is invalid.
Option value constraints:
- retry: must be >= 0
- timeout, delay, cache: must be > 0
- concurrency: must be > 0
- throttle count: must be > 0
→ Use retry: 3 for three retry attempts
→ Use retry: 0 for no retries
See: https://constellation-engine.dev/docs/constellation-lang/module-options
Recovery Strategy:
- Check option value constraints in error message
- Common valid values:
retry: 3— Retry up to 3 timestimeout: 5000— 5 second timeout (milliseconds)cache: 3600000— 1 hour cache (milliseconds)fallback: defaultValue— Use default on failureconcurrency: 10— Max 10 concurrent executionspriority: 5— Priority level (higher = more urgent)
E017: Fallback Type Mismatch
Cause: The fallback value type doesn't match the module's return type.
Example:
in id: Int
# GetAge returns Int, but fallback is String
age = GetAge(id) with fallback: "Unknown"
out age
Error:
Error E017: Fallback type mismatch
--> line 2, column 38
2 │ age = GetAge(id) with fallback: "Unknown"
│ ^^^^^^^^^
Fallback type mismatch: expected Int, got String
The fallback expression type doesn't match the module return type.
The fallback option provides a default value when the module fails.
Its type must be compatible with what the module returns.
Example:
result = GetName(id) with fallback: "Unknown"
If GetName returns String, "Unknown" is valid.
If GetName returns Int, "Unknown" would be invalid.
→ Use fallback: 0 for Int return type
→ Or use fallback: -1 to indicate unknown age
See: https://constellation-engine.dev/docs/constellation-lang/module-options
Recovery Strategy:
- Check the module's return type (hover in VSCode or check signature)
- Provide a fallback value of matching type:
# For Int return type
count = GetCount(id) with fallback: 0
# For String return type
name = GetName(id) with fallback: "Unknown"
# For Boolean return type
flag = CheckStatus(id) with fallback: false
# For Optional return type
value = Fetch(id) with fallback: None
4. Semantic Errors (E030-E039)
Semantic errors occur when code is syntactically valid but has logical issues.
E030: Duplicate Definition
Cause: Defining the same name multiple times in the same scope.
Example:
in data: String
in data: Int # Duplicate input name
out data
Error:
Error E030: Duplicate definition
--> line 2, column 4
1 │ in data: String
2 │ in data: Int
│ ^^^^
Duplicate definition: 'data' is already defined
This name is already defined in the current scope.
Each variable, type, and input must have a unique name.
→ Rename one of the 'data' variables
→ Use data1 and data2 for different inputs
Recovery Strategy:
- Rename one of the conflicting identifiers
- If you meant to reassign, use assignment instead:
in data: String
data2 = Transform(data) # New variable
Common Mistakes:
# WRONG: Duplicate input
in x: Int
in x: String
# CORRECT: Different names
in x: Int
in y: String
# WRONG: Duplicate assignment
result = Step1(data)
result = Step2(data)
# CORRECT: Chain or use different names
intermediate = Step1(data)
result = Step2(intermediate)
# WRONG: Duplicate type definition
type User = { id: Int }
type User = { name: String }
# CORRECT: Different type names or merge fields
type User = { id: Int, name: String }
E031: Circular Dependency
Cause: A variable depends on itself directly or transitively.
Example:
in x: Int
a = b + 1
b = a + 1
out a
Error:
Error E031: Circular dependency
--> line 2, column 1
2 │ a = b + 1
│ ^
3 │ b = a + 1
Circular dependency detected: a -> b -> a
A circular dependency was detected in the DAG.
Variables cannot depend on themselves, directly or indirectly.
→ Break the cycle by removing one of the dependencies
→ Restructure the computation to be acyclic
See: https://constellation-engine.dev/docs/constellation-lang/dag
Recovery Strategy:
- Identify the cycle in the error message
- Break the cycle by:
- Using different input values
- Restructuring the computation
- Removing circular reference
- Remember: Constellation is a DAG (Directed Acyclic Graph) — cycles are not allowed
5. Internal Errors (E900+)
Internal compiler errors indicate bugs in the compiler itself.
E900: Internal Compiler Error
Cause: An unexpected error in the compiler.
Error:
Error E900: Internal compiler error
--> line 3, column 10
An unexpected error occurred in the compiler.
This is a bug in the compiler. Please report it at:
https://github.com/VledicFranco/constellation-engine/issues
→ Include the constellation-lang source that triggered this error
→ Simplify the script to find the minimal reproduction case
Recovery Strategy:
- This is a compiler bug, not your fault
- Try simplifying your script to isolate the issue
- Report the bug with:
- The full source code that triggered the error
- Any error messages or stack traces
- Constellation Engine version (
sbt version)
Runtime Errors
Execution API Errors
From the ApiError hierarchy:
InputError
Cause: Invalid input provided to the pipeline.
Example:
// Providing wrong type
val inputs = Map("count" -> Json.fromString("not a number"))
// Pipeline expects: in count: Int
Error:
{
"error": "INPUT_ERROR",
"message": "Input validation failed for 'count': expected Int, got String"
}
Recovery Strategy:
- Verify JSON input types match constellation-lang declarations:
in count: Int // Expects JSON number
in text: String // Expects JSON string
in flag: Boolean // Expects JSON boolean
in items: List<String> // Expects JSON array - Check for typos in input names (case-sensitive)
- Ensure all required inputs are provided
ExecutionError
Cause: A module threw an exception during execution.
Example:
// Module implementation
.implementationPure[Input, Output] { input =>
if (input.value < 0) throw new IllegalArgumentException("Value must be positive")
Output(input.value * 2)
}
Error:
{
"error": "EXECUTION_ERROR",
"message": "Module 'ValidateInput' execution failed: Value must be positive"
}
Recovery Strategy:
- Check the error message for the underlying cause
- Validate input constraints in your module implementation
- Use
TryorEitherfor safer error handling:.implementation[Input, Output] { input =>
IO {
if (input.value < 0) {
throw new IllegalArgumentException("Value must be positive")
}
Output(input.value * 2)
}.handleErrorWith { err =>
IO.raiseError(new RuntimeException(s"Validation failed: ${err.getMessage}"))
}
} - Use module options for resilience:
result = RiskyModule(data) with fallback: defaultValue
result = UnreliableModule(data) with retry: 3
CompilationError
Cause: Compilation failed with multiple errors.
Error:
{
"error": "COMPILATION_ERROR",
"message": "Compilation failed",
"errors": [
"Undefined variable: textt at line 2",
"Type mismatch: expected String, got Int at line 5"
]
}
Recovery Strategy:
- Fix each error in the list individually
- Start with the first error (later errors may cascade from earlier ones)
- Re-compile after each fix to see remaining errors
NotFoundError
Cause: Referenced resource doesn't exist.
Example:
constellation.getModule("NonExistent")
Error:
{
"error": "NOT_FOUND",
"message": "Module not found: NonExistent"
}
Recovery Strategy:
- Verify the module is registered:
constellation.setModule(myModule) - Check the module name matches exactly (case-sensitive)
- For standard library, ensure StdLib is registered:
StdLib.allModules.values.toList.traverse(constellation.setModule)
Lifecycle Errors
CircuitOpenException
Cause: Circuit breaker is open due to repeated module failures.
Error:
CircuitOpenException: Circuit breaker is open for module: ExternalAPI
Recovery Strategy:
- Wait for the circuit breaker's reset duration (default: 30 seconds)
- Check the underlying module for persistent failures:
- External service down
- Network connectivity issues
- Configuration error
- Monitor circuit breaker state:
val stats = circuitBreaker.stats
println(s"State: ${stats.state}") // Open, HalfOpen, or Closed - Adjust circuit breaker configuration if needed:
CircuitBreakerConfig(
failureThreshold = 5, // Open after 5 failures
resetDuration = 60.seconds, // Wait 60s before retry
halfOpenRequests = 3 // Test with 3 requests
)
QueueFullException
Cause: Scheduler queue is full (too many pending tasks).
Error:
QueueFullException: Scheduler queue is full (max: 100)
Recovery Strategy:
- Implement backpressure in your application:
execution.attempt.flatMap {
case Right(result) => handleResult(result)
case Left(_: QueueFullException) =>
// Wait and retry, or reject request
IO.sleep(100.millis) *> retryExecution
case Left(err) => IO.raiseError(err)
} - Increase queue size:
CONSTELLATION_SCHEDULER_MAX_CONCURRENCY=32 sbt run - Monitor queue depth:
val stats = scheduler.stats
println(s"Queue: ${stats.queuedCount}/${stats.maxQueueSize}")
ShutdownRejectedException
Cause: New execution submitted during graceful shutdown.
Error:
ShutdownRejectedException: Execution rejected: system is shutting down
Recovery Strategy:
- This is expected during shutdown — don't submit new work
- Check lifecycle state before submitting:
if (lifecycle.state == LifecycleState.Running) {
constellation.execute(source, inputs)
} else {
IO.raiseError(new IllegalStateException("System not ready"))
} - Implement graceful degradation:
execution.attempt.flatMap {
case Right(result) => handleSuccess(result)
case Left(_: ShutdownRejectedException) =>
// Return cached result or error response
getCachedResult.orElse(IO.pure(ErrorResponse))
case Left(err) => handleError(err)
}
Suspendable Execution Errors
InputTypeMismatchError
Cause: Resumed execution received input with wrong type.
Example:
// Initial execution declares: in count: Int
val suspended = execution.suspend()
// Later resume with wrong type
val result = execution.resume(
suspended.executionId,
Map("count" -> CValue.CString("not a number")) // Should be CInt
)
Recovery Strategy:
- Verify input types match the pipeline declaration
- Use the correct CValue constructor:
CValue.CInt(42) // For Int
CValue.CString("text") // For String
CValue.CBoolean(true) // For Boolean
CValue.CList(List(...)) // For List<T>
PipelineChangedError
Cause: Pipeline was recompiled between suspend and resume.
Recovery Strategy:
- This is expected if you recompile during execution
- Options:
- Accept that in-flight executions will fail
- Version your pipelines and store version with suspended state
- Drain in-flight executions before recompiling:
// Before recompiling
lifecycle.drain(30.seconds)
// Now safe to recompile
Error Formatting and Presentation
ErrorFormatter
The ErrorFormatter class provides rich error messages:
import io.constellation.lang.compiler.{ErrorFormatter, SuggestionContext}
val source = """
in text: String
result = Uppercase(textt)
out result
"""
val formatter = ErrorFormatter(source)
val context = SuggestionContext(
definedVariables = List("text", "result"),
availableFunctions = List("Uppercase", "Lowercase", "Trim")
)
// Format error with context
val formatted = formatter.format(error, context)
// Output formats
println(formatted.toPlainText) // Terminal output
println(formatted.toMarkdown) // IDE hover tooltip
println(formatted.toOneLine) // Log format
FormattedError Structure
case class FormattedError(
code: String, // "E001"
title: String, // "Undefined variable"
category: ErrorCategory, // Reference, Type, Syntax, etc.
location: String, // "line 2, column 20"
snippet: String, // Code with underline
explanation: String, // Detailed explanation
suggestions: List[String], // "Did you mean?" suggestions
docUrl: Option[String], // Documentation link
rawMessage: String // Original error message
)
Suggestion System
The suggestion system uses Levenshtein distance for "Did you mean?" suggestions:
import io.constellation.lang.compiler.Suggestions
// Find similar strings
val similar = Suggestions.findSimilar(
target = "textt",
candidates = List("text", "count", "result"),
maxDistance = 2, // Max edit distance
maxSuggestions = 3 // Max suggestions to return
)
// Returns: List("text")
// Context-aware suggestions
val suggestions = Suggestions.forError(error, context)
// Returns suggestions based on error type
Levenshtein Distance Examples:
| Target | Candidate | Distance | Match? |
|---|---|---|---|
| "textt" | "text" | 1 | ✓ (extra 't') |
| "Upppercase" | "Uppercase" | 2 | ✓ (extra 'p's) |
| "usr" | "user" | 2 | ✓ (missing 'e') |
| "count" | "amount" | 3 | ✗ (too far) |
Testing Error Scenarios
Parser Error Recovery Tests
import io.constellation.lang.parser.ConstellationParser
import io.constellation.lang.ast.CompileError
// Test that invalid syntax produces ParseError
val source = "result = \nout result" // Missing expression
val result = ConstellationParser.parse(source)
result.isLeft shouldBe true
result.left.toOption.get shouldBe a[CompileError.ParseError]
Type Error Tests
import io.constellation.lang.compiler.{ErrorFormatter, ErrorCodes}
import io.constellation.lang.semantic.TypeChecker
// Test type mismatch detection
val source = """
in count: Int
result = Uppercase(count) # Expects String
out result
"""
val pipeline = ConstellationParser.parse(source).toOption.get
val typeResult = TypeChecker.check(pipeline, registry)
typeResult.isLeft shouldBe true
val errors = typeResult.left.toOption.get
errors.head shouldBe a[CompileError.TypeMismatch]
// Test formatted error
val formatter = ErrorFormatter(source)
val formatted = formatter.format(errors.head)
formatted.code shouldBe "E010"
formatted.category shouldBe ErrorCategory.Type
formatted.suggestions should not be empty
Runtime Error Tests
import cats.effect.IO
import io.constellation.ModuleBuilder
// Test module execution error handling
val failingModule = ModuleBuilder
.metadata("FailingModule", "Always fails", 1, 0)
.implementation[Input, Output] { _ =>
IO.raiseError(new RuntimeException("Simulated failure"))
}
.build
// Test with fallback
val source = """
in data: String
result = FailingModule(data) with fallback: "default"
out result
"""
// Should use fallback instead of failing
val result = constellation.execute(source, inputs).unsafeRunSync()
result shouldBe Right(Map("result" -> CValue.CString("default")))
// Test without fallback (should fail)
val sourceNoFallback = """
in data: String
result = FailingModule(data)
out result
"""
val result2 = constellation.execute(sourceNoFallback, inputs).attempt.unsafeRunSync()
result2.isLeft shouldBe true
Circuit Breaker Tests
import io.constellation.execution.CircuitBreakerConfig
// Test circuit breaker opening after failures
val config = CircuitBreakerConfig(
failureThreshold = 3,
resetDuration = 1.second
)
// Simulate multiple failures
(1 to 3).foreach { _ =>
module.execute(input).attempt.unsafeRunSync()
}
// Circuit should now be open
val stats = circuitBreaker.stats
stats.state shouldBe CircuitState.Open
// Further calls should be rejected
val result = module.execute(input).attempt.unsafeRunSync()
result.isLeft shouldBe true
result.left.toOption.get shouldBe a[CircuitOpenException]
// Wait for reset
Thread.sleep(1100)
// Circuit should be half-open
val stats2 = circuitBreaker.stats
stats2.state shouldBe CircuitState.HalfOpen
Error Reporting Best Practices
1. Always Provide Location Information
// GOOD: Include span for precise error location
CompileError.UndefinedVariable("textt", Some(Span(45, 50)))
// BAD: No location information
CompileError.UndefinedVariable("textt", None)
2. Include Context in Error Messages
// GOOD: Specific context
s"Type mismatch in argument 1 of Uppercase: expected String, got Int"
// BAD: Vague error
"Type mismatch"
3. Provide Actionable Suggestions
// GOOD: Tell user how to fix it
"Did you mean 'text'? Variable names are case-sensitive."
// BAD: Just state the problem
"Variable not found"
4. Use Structured Error Types
// GOOD: Use specific error types
case class TypeMismatchError(
expected: CType,
actual: CType,
location: Option[Span]
) extends CompileError
// BAD: Generic errors
case class GenericError(message: String) extends CompileError
5. Test Error Paths
// GOOD: Dedicated tests for error cases
"TypeChecker" should "reject type mismatch in function arguments" in {
val result = check("result = Uppercase(42)")
result.isLeft shouldBe true
result.left.toOption.get.head shouldBe a[CompileError.TypeMismatch]
}
// GOOD: Test error recovery
"Parser" should "recover from unclosed parenthesis" in {
val result = parse("result = func(\nout result")
result.isLeft shouldBe true
result.left.toOption.get shouldBe a[CompileError.ParseError]
}
6. Format Errors Consistently
// Use ErrorFormatter for all user-facing errors
val formatted = ErrorFormatter(source).format(error, context)
// Terminal output
println(formatted.toPlainText)
// IDE integration
sendDiagnostic(formatted.toMarkdown)
// Logging
logger.error(formatted.toOneLine)
7. Aggregate Related Errors
// GOOD: Return all errors at once
def check(pipeline: Pipeline): Either[List[CompileError], TypedPipeline]
// BAD: Fail on first error (makes debugging slower)
def check(pipeline: Pipeline): Either[CompileError, TypedPipeline]
Common Debugging Workflows
Workflow 1: Fixing Parse Errors
- Read the error location — Line and column point to problem
- Check for common syntax mistakes:
- Missing parentheses:
func(x→func(x) - Missing commas:
func(x y)→func(x, y) - Unclosed strings:
"text→"text" - Invalid identifiers:
123var→var123
- Missing parentheses:
- Look at the previous line — Parse errors often point to line after the mistake
- Use IDE syntax highlighting — Mismatched delimiters show up visually
Workflow 2: Fixing Type Errors
- Read expected vs actual types — Error shows what was expected and what was found
- Hover in VSCode to see inferred types — Verify upstream types are correct
- Check function signatures:
// In Scala: see ModuleBuilder.metadata()
FunctionSignature(
"Uppercase",
List(FunctionParameter("text", CType.CString)),
CType.CString
) - Use type conversions:
ToString(value)for StringToInt(value)for Int- Int auto-promotes to Float
- Break complex expressions into steps:
# Instead of:
result = Complex(Transform(Validate(input)))
# Break down:
validated = Validate(input)
transformed = Transform(validated)
result = Complex(transformed)
# Now you can see exactly where the type mismatch occurs
Workflow 3: Fixing Runtime Errors
- Check the error message for underlying cause
- Verify module registration:
constellation.setModule(myModule) - Check input types match declarations:
// For: in count: Int
{ "count": 42 } // Not "42" - Use fallback for unreliable modules:
result = RiskyModule(data) with fallback: defaultValue - Add retry for transient failures:
result = UnreliableAPI(data) with retry: 3 - Check circuit breaker state:
circuitBreaker.stats.state // Open, HalfOpen, Closed
Workflow 4: Debugging DAG Issues
- Visualize the DAG:
val dag = compiler.compile(source).toOption.get
println(DagRenderer.render(dag)) // ASCII visualization - Check for cycles:
- Error will show cycle path:
a -> b -> c -> a - Break the cycle by restructuring
- Error will show cycle path:
- Verify execution order:
- DAG determines execution order based on dependencies
- Use
Runtime.State.moduleStatusesto see execution status
- Check data flow:
- Each node must have all inputs available before execution
- Missing data indicates upstream failure
Advanced Error Handling Patterns
Pattern 1: Cascading Error Recovery
# Try primary source, fallback to secondary, then default
primary = FetchPrimary(id) with fallback: None
secondary = FetchSecondary(id) with fallback: None
data = primary ?? secondary ?? defaultData
out data
Pattern 2: Conditional Error Handling
// In module implementation
.implementation[Input, Output] { input =>
IO {
if (input.value < 0) {
throw ValidationException("Value must be positive")
}
if (input.value > 1000) {
throw ValidationException("Value too large")
}
Output(input.value)
}.handleErrorWith {
case e: ValidationException =>
// Return default for validation errors
IO.pure(Output(0))
case e: IOException =>
// Retry for IO errors
IO.sleep(1.second) *> IO.raiseError(e)
case e =>
// Fail for unexpected errors
IO.raiseError(e)
}
}
Pattern 3: Error Aggregation
// Collect multiple validation errors
def validateInputs(inputs: Map[String, CValue]): Either[List[ValidationError], Unit] = {
val errors = List.newBuilder[ValidationError]
inputs.get("count") match {
case Some(CValue.CInt(n)) if n < 0 =>
errors += ValidationError("count must be positive")
case None =>
errors += ValidationError("count is required")
case _ => ()
}
inputs.get("name") match {
case Some(CValue.CString(s)) if s.isEmpty =>
errors += ValidationError("name cannot be empty")
case None =>
errors += ValidationError("name is required")
case _ => ()
}
val allErrors = errors.result()
if (allErrors.isEmpty) Right(()) else Left(allErrors)
}
Pattern 4: Graceful Degradation
# Try enhanced version, fall back to basic version
enhanced = EnhancedProcessor(data) with
timeout: 5000
fallback: None
basic = BasicProcessor(data) with fallback: None
result = enhanced ?? basic ?? minimalResult
out result
Error Recovery Decision Tree
┌─────────────────────────┐
│ Compilation Failed │
└────────────┬────────────┘
│
├── Parse Error (E020-E029)
│ ├── Check syntax near error line
│ ├── Look for unclosed delimiters
│ └── Verify keyword spelling
│
├── Reference Error (E001-E009)
│ ├── Check for typos ("Did you mean?")
│ ├── Verify declaration order
│ └── Check module registration
│
├── Type Error (E010-E019)
│ ├── Check expected vs actual types
│ ├── Use type conversion functions
│ └── Break complex expressions into steps
│
└── Semantic Error (E030-E039)
├── Rename duplicate identifiers
└── Break circular dependencies
┌─────────────────────────┐
│ Execution Failed │
└────────────┬────────────┘
│
├── Module Not Found
│ ├── Register module: constellation.setModule()
│ └── Check name matches exactly
│
├── Module Execution Error
│ ├── Check error message for cause
│ ├── Verify input data
│ └── Use fallback or retry options
│
├── Input Validation Error
│ ├── Verify JSON types match declarations
│ ├── Check all required inputs provided
│ └── Validate input constraints
│
├── Circuit Open
│ ├── Wait for reset duration
│ ├── Check underlying service
│ └── Adjust circuit breaker config
│
└── Queue Full
├── Implement backpressure
├── Increase concurrency/queue size
└── Monitor queue depth
Related
- Language Error Messages — User-facing error documentation
- Error Reference — Complete error catalog
- Type System — Understanding constellation-lang types
- Module Options — Error handling with options (retry, fallback, etc.)