Boundary Error Events
Boundary error events are BPMN elements that catch errors thrown by tasks, enabling controlled error handling and recovery strategies.
Overview
Boundary events allow you to:
- ✅ Catch specific errors by error code
- ✅ Choose interrupting or non-interrupting behavior
- ✅ Implement recovery logic for different error scenarios
- ✅ Prevent process crashes from unhandled errors
Boundary Event Types
Interrupting Boundary Events
Interrupting boundary events cancel the task when an error occurs:
<bpmn:scriptTask id="ProcessPayment" name="Process Payment">
<bpmn:script>
if (context.accountBalance < context.paymentAmount) {
throw new BpmnError('INSUFFICIENT_FUNDS',
'Account balance $' + context.accountBalance +
' is less than payment amount $' + context.paymentAmount);
}
// Process payment...
</bpmn:script>
</bpmn:scriptTask>
<!-- Interrupting boundary - cancels task -->
<bpmn:boundaryEvent id="InsufficientFunds"
attachedToRef="ProcessPayment"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="InsufficientFundsError" />
</bpmn:boundaryEvent>
<!-- Error handler -->
<bpmn:scriptTask id="NotifyInsufficientFunds" name="Notify Customer">
<bpmn:script>
var error = context._lastError;
BankLingo.ExecuteCommand('SendEmail', {
to: context.customerEmail,
subject: 'Payment Failed - Insufficient Funds',
body: error.errorMessage
});
context.paymentStatus = 'FAILED';
context.failureReason = error.errorMessage;
</bpmn:script>
</bpmn:scriptTask>
<bpmn:error id="InsufficientFundsError" errorCode="INSUFFICIENT_FUNDS" />
Characteristics:
- ✅ Task is canceled immediately
- ✅ Process follows error path
- ✅ Original task output is lost
- ✅ Use for: Unrecoverable errors, critical failures
Non-Interrupting Boundary Events
Non-interrupting boundary events handle errors without canceling the task:
<bpmn:userTask id="SubmitApplication" name="Submit Application">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="application-form"/>
<custom:property name="ServerScript"><![CDATA[
var errors = [];
if (!formData.email || !formData.email.includes('@')) {
errors.push('Valid email is required');
}
if (!formData.phoneNumber || formData.phoneNumber.length < 10) {
errors.push('Valid phone number is required');
}
if (errors.length > 0) {
throw new BpmnError('VALIDATION_ERROR', errors.join('; '));
}
// Save data if valid
context.email = formData.email;
context.phoneNumber = formData.phoneNumber;
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Non-interrupting boundary - task continues -->
<bpmn:boundaryEvent id="ValidationError"
attachedToRef="SubmitApplication"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="ValidationError" />
</bpmn:boundaryEvent>
<!-- Send validation errors to UI (task still active) -->
<bpmn:scriptTask id="NotifyValidationError" name="Notify Validation Error">
<bpmn:script>
var error = context._lastError;
// Send errors back to form UI
BankLingo.ExecuteCommand('SendFormErrors', {
taskId: 'SubmitApplication',
errors: error.errorMessage
});
logger.info('Form validation errors sent to UI');
</bpmn:script>
</bpmn:scriptTask>
<!-- User can correct and resubmit -->
<bpmn:error id="ValidationError" errorCode="VALIDATION_ERROR" />
Characteristics:
- ✅ Task continues running
- ✅ Error handled in parallel
- ✅ Multiple error handlers can run
- ✅ Use for: Validation errors, warnings, logging
Error Matching
Match by Error Code
Catch specific error codes:
<bpmn:scriptTask id="ValidateCredit" name="Validate Credit">
<bpmn:script>
if (context.creditScore < 600) {
throw new BpmnError('CREDIT_SCORE_TOO_LOW',
'Credit score ' + context.creditScore + ' is below minimum 600');
}
if (context.creditScore === null) {
throw new BpmnError('CREDIT_DATA_UNAVAILABLE',
'Credit bureau data unavailable');
}
</bpmn:script>
</bpmn:scriptTask>
<!-- Catch low credit score -->
<bpmn:boundaryEvent id="LowCreditScore"
attachedToRef="ValidateCredit"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="LowCreditError" />
</bpmn:boundaryEvent>
<bpmn:scriptTask id="HandleLowCredit" name="Handle Low Credit">
<bpmn:script>
context.decisionReason = 'Credit score too low';
context.requiresManualReview = true;
</bpmn:script>
</bpmn:scriptTask>
<!-- Catch unavailable credit data -->
<bpmn:boundaryEvent id="CreditDataUnavailable"
attachedToRef="ValidateCredit"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="CreditDataError" />
</bpmn:boundaryEvent>
<bpmn:scriptTask id="HandleCreditDataError" name="Retry Credit Check">
<bpmn:script>
context.creditCheckRetries = (context.creditCheckRetries || 0) + 1;
if (context.creditCheckRetries < 3) {
// Loop back to retry
context.retryCreditCheck = true;
} else {
// Give up after 3 retries
throw new BpmnError('CREDIT_CHECK_FAILED',
'Credit check failed after 3 attempts');
}
</bpmn:script>
</bpmn:scriptTask>
<bpmn:error id="LowCreditError" errorCode="CREDIT_SCORE_TOO_LOW" />
<bpmn:error id="CreditDataError" errorCode="CREDIT_DATA_UNAVAILABLE" />
Catch All Errors
Catch any error with a generic boundary:
<bpmn:serviceTask id="CallExternalAPI" name="Call External API">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CallPartnerAPICommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Catch all errors (no errorRef specified) -->
<bpmn:boundaryEvent id="AnyError"
attachedToRef="CallExternalAPI"
cancelActivity="true">
<bpmn:errorEventDefinition />
</bpmn:boundaryEvent>
<!-- Generic error handler -->
<bpmn:scriptTask id="HandleAnyError" name="Handle Any Error">
<bpmn:script>
var error = context._lastError;
logger.error('API call failed: ' + error.errorCode + ' - ' + error.errorMessage);
// Determine action based on error code
switch (error.errorCode) {
case 'GATEWAY_TIMEOUT':
case 'GATEWAY_ERROR':
context.errorAction = 'RETRY';
break;
case 'AUTHENTICATION_ERROR':
context.errorAction = 'REFRESH_TOKEN';
break;
default:
context.errorAction = 'ESCALATE';
}
</bpmn:script>
</bpmn:scriptTask>
Multiple Boundary Events
Attach multiple boundaries for different error scenarios:
<bpmn:serviceTask id="ProcessTransaction" name="Process Transaction">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="ProcessTransactionCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Boundary 1: Insufficient funds -->
<bpmn:boundaryEvent id="InsufficientFunds"
attachedToRef="ProcessTransaction"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="InsufficientFundsError" />
</bpmn:boundaryEvent>
<bpmn:scriptTask id="NotifyInsufficientFunds" name="Notify Customer">
<!-- Handler for insufficient funds -->
</bpmn:scriptTask>
<!-- Boundary 2: Transaction declined -->
<bpmn:boundaryEvent id="TransactionDeclined"
attachedToRef="ProcessTransaction"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="DeclinedError" />
</bpmn:boundaryEvent>
<bpmn:scriptTask id="HandleDeclined" name="Handle Declined">
<!-- Handler for declined transaction -->
</bpmn:scriptTask>
<!-- Boundary 3: Gateway timeout (retryable) -->
<bpmn:boundaryEvent id="GatewayTimeout"
attachedToRef="ProcessTransaction"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="TimeoutError" />
</bpmn:boundaryEvent>
<bpmn:scriptTask id="RetryTransaction" name="Retry Transaction">
<!-- Non-interrupting retry handler -->
</bpmn:scriptTask>
<!-- Boundary 4: Any other error -->
<bpmn:boundaryEvent id="OtherError"
attachedToRef="ProcessTransaction"
cancelActivity="true">
<bpmn:errorEventDefinition />
</bpmn:boundaryEvent>
<bpmn:scriptTask id="HandleOtherError" name="Handle Other Error">
<!-- Generic error handler -->
</bpmn:scriptTask>
<bpmn:error id="InsufficientFundsError" errorCode="INSUFFICIENT_FUNDS" />
<bpmn:error id="DeclinedError" errorCode="TRANSACTION_DECLINED" />
<bpmn:error id="TimeoutError" errorCode="GATEWAY_TIMEOUT" />
Matching Priority:
- Specific error code match (highest priority)
- Generic catch-all boundary (lowest priority)
- First matching boundary wins (if multiple match)
Common Patterns
Pattern 1: Retry with Boundary Events
<bpmn:serviceTask id="CallAPI" name="Call External API">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CallAPICommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Non-interrupting for retryable errors -->
<bpmn:boundaryEvent id="RetryableError"
attachedToRef="CallAPI"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="RetryableError" />
</bpmn:boundaryEvent>
<!-- Interrupting for permanent errors -->
<bpmn:boundaryEvent id="PermanentError"
attachedToRef="CallAPI"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="PermanentError" />
</bpmn:boundaryEvent>
<!-- Retry logic -->
<bpmn:scriptTask id="CalculateRetry" name="Calculate Retry">
<bpmn:script>
var retryCount = (context.apiRetryCount || 0) + 1;
var maxRetries = 3;
if (retryCount > maxRetries) {
// Convert to permanent error
throw new BpmnError('API_RETRY_EXHAUSTED',
'Failed after ' + maxRetries + ' attempts');
}
context.apiRetryCount = retryCount;
// Exponential backoff: 2^n * 1000ms
context.retryDelayMs = Math.pow(2, retryCount) * 1000;
logger.info('Retry ' + retryCount + ' after ' + context.retryDelayMs + 'ms');
</bpmn:script>
</bpmn:scriptTask>
<!-- Wait before retry -->
<bpmn:intermediateCatchEvent id="WaitBeforeRetry" name="Wait">
<bpmn:timerEventDefinition>
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">
PT${context.retryDelayMs}MS
</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<!-- Loop back -->
<bpmn:sequenceFlow sourceRef="WaitBeforeRetry" targetRef="CallAPI" />
<!-- Handle permanent failure -->
<bpmn:scriptTask id="HandlePermanentError" name="Handle Permanent Error">
<bpmn:script>
var error = context._lastError;
logger.error('API permanently failed: ' + error.errorMessage);
context.apiCallFailed = true;
context.useAlternativeMethod = true;
</bpmn:script>
</bpmn:scriptTask>
<bpmn:error id="RetryableError" errorCode="GATEWAY_TIMEOUT" />
<bpmn:error id="PermanentError" errorCode="API_RETRY_EXHAUSTED" />
Pattern 2: Escalation on Error
<bpmn:userTask id="TeamLeadApproval" name="Team Lead Approval">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form"/>
<custom:property name="ServerScript"><![CDATA[
// Check approval authority
if (context.amount > 50000) {
throw new BpmnError('INSUFFICIENT_AUTHORITY',
'Amount exceeds team lead approval limit of $50,000');
}
context.approvedBy = formData.userId;
context.approvalComments = formData.comments;
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Escalate to manager -->
<bpmn:boundaryEvent id="EscalateToManager"
attachedToRef="TeamLeadApproval"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="InsufficientAuthorityError" />
</bpmn:boundaryEvent>
<!-- Manager approval task -->
<bpmn:userTask id="ManagerApproval" name="Manager Approval (Escalated)">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form"/>
<custom:property name="ResponsibleTeams" value="Managers"/>
<custom:property name="Description" value="ESCALATED: Amount exceeds team lead authority"/>
<custom:property name="Priority" value="HIGH"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<bpmn:error id="InsufficientAuthorityError" errorCode="INSUFFICIENT_AUTHORITY" />
Pattern 3: Validation Loop
<bpmn:userTask id="CustomerForm" name="Customer Form">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="customer-form"/>
<custom:property name="ServerScript"><![CDATA[
var errors = [];
// Validate email
if (!formData.email || !formData.email.includes('@')) {
errors.push('Valid email is required');
}
// Validate phone
if (!formData.phone || formData.phone.length < 10) {
errors.push('Valid 10-digit phone number is required');
}
// Validate age
if (!formData.age || formData.age < 18) {
errors.push('Must be 18 years or older');
}
if (errors.length > 0) {
throw new BpmnError('VALIDATION_ERROR', errors.join('; '));
}
// Save valid data
context.customerEmail = formData.email;
context.customerPhone = formData.phone;
context.customerAge = formData.age;
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Non-interrupting validation error -->
<bpmn:boundaryEvent id="ValidationError"
attachedToRef="CustomerForm"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="ValidationError" />
</bpmn:boundaryEvent>
<!-- Send errors to UI -->
<bpmn:scriptTask id="SendValidationErrors" name="Send Validation Errors">
<bpmn:script>
var error = context._lastError;
// Send to UI for display
BankLingo.ExecuteCommand('SendFormErrors', {
taskId: 'CustomerForm',
errors: error.errorMessage
});
// Track validation attempts
context.validationAttempts = (context.validationAttempts || 0) + 1;
if (context.validationAttempts > 5) {
logger.warn('Customer exceeded 5 validation attempts');
// Could send help email or close form
}
</bpmn:script>
</bpmn:scriptTask>
<!-- User corrects and resubmits (task still active) -->
<bpmn:error id="ValidationError" errorCode="VALIDATION_ERROR" />
Pattern 4: Fallback on Error
<bpmn:serviceTask id="PrimaryService" name="Call Primary Service">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="PrimaryServiceCommand"/>
<custom:property name="ResultVariable" value="serviceResult"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Catch primary service error -->
<bpmn:boundaryEvent id="PrimaryServiceError"
attachedToRef="PrimaryService"
cancelActivity="true">
<bpmn:errorEventDefinition />
</bpmn:boundaryEvent>
<!-- Try fallback service -->
<bpmn:serviceTask id="FallbackService" name="Call Fallback Service">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="FallbackServiceCommand"/>
<custom:property name="ResultVariable" value="serviceResult"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Catch fallback error -->
<bpmn:boundaryEvent id="FallbackServiceError"
attachedToRef="FallbackService"
cancelActivity="true">
<bpmn:errorEventDefinition />
</bpmn:boundaryEvent>
<!-- Use cached data as last resort -->
<bpmn:scriptTask id="UseCachedData" name="Use Cached Data">
<bpmn:script>
logger.warn('Both primary and fallback services failed. Using cached data.');
context.serviceResult = context.cachedServiceData || {};
context.dataSource = 'CACHE';
context.dataFreshness = 'STALE';
context.cacheAge = Date.now() - new Date(context.cacheTimestamp).getTime();
if (!context.cachedServiceData) {
throw new BpmnError('NO_DATA_AVAILABLE',
'All data sources failed and no cache available');
}
</bpmn:script>
</bpmn:scriptTask>
<!-- Final error if even cache fails -->
<bpmn:boundaryEvent id="NoDataAvailable"
attachedToRef="UseCachedData"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="NoDataError" />
</bpmn:boundaryEvent>
<bpmn:error id="NoDataError" errorCode="NO_DATA_AVAILABLE" />
Best Practices
✅ Do This
<!-- ✅ Use interrupting for unrecoverable errors -->
<bpmn:boundaryEvent cancelActivity="true">
<bpmn:errorEventDefinition errorRef="FatalError"/>
</bpmn:boundaryEvent>
<!-- ✅ Use non-interrupting for validation errors -->
<bpmn:boundaryEvent cancelActivity="false">
<bpmn:errorEventDefinition errorRef="ValidationError"/>
</bpmn:boundaryEvent>
<!-- ✅ Catch specific errors -->
<bpmn:boundaryEvent>
<bpmn:errorEventDefinition errorRef="SpecificError"/>
</bpmn:boundaryEvent>
<!-- ✅ Add generic catch-all as fallback -->
<bpmn:boundaryEvent>
<bpmn:errorEventDefinition/> <!-- Catches all errors -->
</bpmn:boundaryEvent>
<!-- ✅ Log errors in handlers -->
<bpmn:script>
var error = context._lastError;
logger.error('Error caught: ' + error.errorCode);
</bpmn:script>
❌ Don't Do This
<!-- ❌ Missing boundary events -->
<bpmn:scriptTask id="RiskyTask">
<!-- No error boundaries - errors crash process -->
</bpmn:scriptTask>
<!-- ❌ Wrong cancelActivity for use case -->
<bpmn:boundaryEvent cancelActivity="true"> <!-- Should be false -->
<bpmn:errorEventDefinition errorRef="ValidationError"/>
</bpmn:boundaryEvent>
<!-- ❌ Too many catch-alls (makes debugging hard) -->
<bpmn:boundaryEvent>
<bpmn:errorEventDefinition/> <!-- Which errors? -->
</bpmn:boundaryEvent>
<!-- ❌ No error handling in boundary task -->
<bpmn:scriptTask id="ErrorHandler">
<bpmn:script>
// No access to error context, no logging
return { handled: true };
</bpmn:script>
</bpmn:scriptTask>
Error Definition
Define errors in BPMN XML:
<!-- Error definitions -->
<bpmn:error id="ValidationError" errorCode="VALIDATION_ERROR" />
<bpmn:error id="InsufficientFundsError" errorCode="INSUFFICIENT_FUNDS" />
<bpmn:error id="TimeoutError" errorCode="GATEWAY_TIMEOUT" />
<bpmn:error id="AuthenticationError" errorCode="AUTHENTICATION_ERROR" />
Properties:
id: Unique identifier for BPMN referenceserrorCode: The code thrown by BpmnError (must match)
Related Documentation
- Error Handling Overview - Complete error handling guide
- JavaScript Error Throwing - BpmnError constructor
- Error Recovery Patterns - Recovery strategies
- Script Task Errors - Script task patterns
- Service Task Errors - Service task patterns
- User Task Validation - Form validation
Features Used:
- Phase 5: Error Handling
Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026