Skip to main content

JavaScript Error Throwing (Phase 5)

Learn how to throw BPMN-aware errors from JavaScript code that can be caught by boundary error events.

Overview

Phase 5 introduces the BpmnError constructor in JavaScript, allowing scripts to throw structured errors that integrate seamlessly with BPMN error handling mechanisms.

Key Benefits:

  • Structured error handling with error codes
  • Boundary event catching for recovery patterns
  • Context preservation with error history
  • Type-safe error matching with errorRef
  • Audit trail of all errors

Quick Example

// In any ScriptTask
if (context.amount > 1000000) {
throw new BpmnError('AMOUNT_EXCEEDED', 'Amount exceeds maximum limit of $1,000,000');
}
<!-- BPMN: Catch the error -->
<bpmn:boundaryEvent id="AmountError" attachedToRef="ValidateAmount" cancelActivity="true">
<bpmn:errorEventDefinition errorRef="AmountExceededError" />
</bpmn:boundaryEvent>

<bpmn:error id="AmountExceededError" name="Amount Exceeded" errorCode="AMOUNT_EXCEEDED" />

BpmnError Constructor

Syntax

throw new BpmnError(errorCode, errorMessage);

Parameters

ParameterTypeRequiredDescription
errorCodestringYesError identifier for matching (e.g., "API_ERROR", "VALIDATION_ERROR")
errorMessagestringYesHuman-readable error description

Examples

// Validation error
throw new BpmnError('VALIDATION_ERROR', 'Customer ID is required');

// Business rule violation
throw new BpmnError('INSUFFICIENT_FUNDS',
`Balance $${account.balance} is less than required $${context.amount}`);

// External API failure
throw new BpmnError('API_ERROR',
`Partner API returned status ${response.status}: ${response.message}`);

// Timeout error
throw new BpmnError('SERVICE_TIMEOUT',
'External service did not respond within 30 seconds');

// Data integrity error
throw new BpmnError('DATA_ERROR',
`Expected ${expectedCount} records but found ${actualCount}`);

Common Error Codes

Recommended standard error codes:

Error CodeUsageExample
VALIDATION_ERRORInput validation failuresMissing required field
INSUFFICIENT_FUNDSBalance/credit checksAccount balance too low
API_ERRORExternal API failuresREST API returned 500
SERVICE_TIMEOUTTimeout conditionsService did not respond
NOT_FOUNDResource not foundCustomer ID not found
UNAUTHORIZEDPermission deniedUser lacks required role
DATA_ERRORData integrity issuesInvalid data format
BUSINESS_RULE_VIOLATIONBusiness rule failuresApproval limit exceeded
CONFIGURATION_ERRORSystem misconfigurationMissing API key
EXTERNAL_SYSTEM_ERRORThird-party failuresPayment gateway down

Integration with Script Tasks

Basic ScriptTask with Error

<bpmn:scriptTask id="ValidateCustomer" name="Validate Customer">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script"><![CDATA[
logger.info('Validating customer: ' + context.customerId);

// Check required fields
if (!context.customerId) {
throw new BpmnError('VALIDATION_ERROR', 'Customer ID is required');
}

if (!context.email) {
throw new BpmnError('VALIDATION_ERROR', 'Email address is required');
}

// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(context.email)) {
throw new BpmnError('VALIDATION_ERROR',
'Invalid email format: ' + context.email);
}

// Validate age
if (context.age < 18) {
throw new BpmnError('BUSINESS_RULE_VIOLATION',
'Customer must be at least 18 years old');
}

logger.info('Validation successful');
context.validationPassed = true;
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

<!-- Catch validation errors -->
<bpmn:boundaryEvent id="ValidationFailed"
attachedToRef="ValidateCustomer"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="ValidationError" />
</bpmn:boundaryEvent>

<!-- Error definition -->
<bpmn:error id="ValidationError"
name="Validation Error"
errorCode="VALIDATION_ERROR" />

ServiceTask Integration

<bpmn:serviceTask id="CallPartnerAPI" name="Call Partner API">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="command" value="CallExternalAPI"/>
<custom:property name="preScript"><![CDATA[
// Validate before calling API
if (!context.apiKey) {
throw new BpmnError('CONFIGURATION_ERROR',
'API key not configured');
}

if (!context.endpoint) {
throw new BpmnError('CONFIGURATION_ERROR',
'API endpoint not configured');
}
]]></custom:property>
<custom:property name="postScript"><![CDATA[
// Validate API response
if (!result.success) {
throw new BpmnError('API_ERROR',
'API call failed: ' + result.error);
}

if (!result.data) {
throw new BpmnError('DATA_ERROR',
'API returned no data');
}

context.apiResult = result.data;
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Catch API errors -->
<bpmn:boundaryEvent id="APIError"
attachedToRef="CallPartnerAPI"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="APIError" />
</bpmn:boundaryEvent>

Error Context Access

After a BpmnError is thrown and caught, the error details are available in context._lastError:

// In error handler ScriptTask
var error = context._lastError;

logger.error('Error Code: ' + error.errorCode);
logger.error('Error Message: ' + error.errorMessage);
logger.error('Source Task: ' + error.sourceElement);
logger.error('Caught By: ' + error.caughtBy);
logger.error('Timestamp: ' + error.timestamp);

// Error object properties
error.errorCode // "API_ERROR"
error.errorMessage // "Partner API connection failed"
error.sourceElement // "CallPartnerAPI" (task ID)
error.caughtBy // "HandleAPIError" (boundary event ID)
error.timestamp // "2026-01-10T10:30:00Z"
error.exceptionDetails // Full stack trace

Error History

All errors are tracked in context._errorHistory:

// Access error history
var errorHistory = context._errorHistory || [];

logger.info('Total errors: ' + errorHistory.length);

// Analyze error patterns
var apiErrors = errorHistory.filter(e => e.errorCode === 'API_ERROR');
logger.warn('API errors count: ' + apiErrors.length);

// Check for repeated errors
if (apiErrors.length >= 3) {
logger.error('Circuit breaker triggered - too many API failures');
throw new BpmnError('CIRCUIT_BREAKER_OPEN',
'Service unavailable after multiple failures');
}

Error Matching

Specific Error Code Matching

<!-- Catch only VALIDATION_ERROR -->
<bpmn:boundaryEvent id="ValidationError"
attachedToRef="ValidateInput"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="ValidationError" />
</bpmn:boundaryEvent>

<bpmn:error id="ValidationError" errorCode="VALIDATION_ERROR" />

Catch-All Error Handler

<!-- Catch any error (no errorRef specified) -->
<bpmn:boundaryEvent id="AnyError"
attachedToRef="RiskyTask"
cancelActivity="true">
<bpmn:errorEventDefinition /> <!-- No errorRef = catch all -->
</bpmn:boundaryEvent>

Multiple Error Handlers

<bpmn:scriptTask id="ProcessPayment" name="Process Payment">
<!-- ... -->
</bpmn:scriptTask>

<!-- Specific handler for insufficient funds -->
<bpmn:boundaryEvent id="InsufficientFunds"
attachedToRef="ProcessPayment"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="InsufficientFundsError" />
</bpmn:boundaryEvent>

<!-- Specific handler for API errors -->
<bpmn:boundaryEvent id="PaymentAPIError"
attachedToRef="ProcessPayment"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="APIError" />
</bpmn:boundaryEvent>

<!-- Catch-all for other errors -->
<bpmn:boundaryEvent id="OtherError"
attachedToRef="ProcessPayment"
cancelActivity="true">
<bpmn:errorEventDefinition />
</bpmn:boundaryEvent>

<!-- Error definitions -->
<bpmn:error id="InsufficientFundsError" errorCode="INSUFFICIENT_FUNDS" />
<bpmn:error id="APIError" errorCode="API_ERROR" />

Common Patterns

Pattern 1: Validation with Error

// Comprehensive validation
function validateLoanApplication(context) {
const errors = [];

if (!context.applicantId) errors.push('Applicant ID required');
if (!context.loanAmount) errors.push('Loan amount required');
if (context.loanAmount < 1000) errors.push('Minimum loan amount is $1,000');
if (context.loanAmount > 1000000) errors.push('Maximum loan amount is $1,000,000');
if (!context.applicantEmail) errors.push('Email required');

if (errors.length > 0) {
throw new BpmnError('VALIDATION_ERROR',
'Validation failed: ' + errors.join(', '));
}

return true;
}

// Use in script
validateLoanApplication(context);
logger.info('Validation passed');

Pattern 2: API Call with Error Handling

// Call external API with error handling
var response = BankLingo.ExecuteCommand('CallExternalAPI', {
url: context.apiUrl,
method: 'POST',
data: {
customerId: context.customerId,
amount: context.amount
},
timeout: 30000
});

// Check for errors
if (!response.success) {
if (response.statusCode === 404) {
throw new BpmnError('NOT_FOUND',
'Customer not found in external system');
} else if (response.statusCode === 401) {
throw new BpmnError('UNAUTHORIZED',
'API authentication failed');
} else if (response.timeout) {
throw new BpmnError('SERVICE_TIMEOUT',
'API did not respond within 30 seconds');
} else {
throw new BpmnError('API_ERROR',
`API error: ${response.statusCode} - ${response.error}`);
}
}

// Process successful response
context.externalData = response.data;

Pattern 3: Business Rule Validation

// Check business rules
var account = BankLingo.ExecuteCommand('GetAccount', {
accountId: context.accountId
});

if (!account.active) {
throw new BpmnError('BUSINESS_RULE_VIOLATION',
'Cannot process transaction - account is inactive');
}

if (account.balance < context.amount) {
throw new BpmnError('INSUFFICIENT_FUNDS',
`Insufficient balance: $${account.balance} available, $${context.amount} required`);
}

if (context.amount > account.dailyLimit) {
throw new BpmnError('BUSINESS_RULE_VIOLATION',
`Transaction amount $${context.amount} exceeds daily limit $${account.dailyLimit}`);
}

// Proceed with transaction
logger.info('Business rules validated successfully');

Pattern 4: Retry with Error Tracking

// Initialize retry counter
context.retryCount = (context.retryCount || 0) + 1;
var maxRetries = 3;

logger.info(`Attempt ${context.retryCount}/${maxRetries}`);

try {
// Attempt operation
var result = BankLingo.ExecuteCommand('RiskyOperation', context.data);

if (!result.success) {
throw new Error('Operation failed: ' + result.error);
}

// Success - reset counter
context.retryCount = 0;
context.operationResult = result;

} catch (ex) {
if (context.retryCount < maxRetries) {
// Retry - throw error to trigger retry flow
logger.warn(`Attempt ${context.retryCount} failed, will retry`);
throw new BpmnError('RETRY_REQUIRED',
`Operation failed on attempt ${context.retryCount}: ${ex.message}`);
} else {
// Max retries exceeded - permanent failure
logger.error('Max retries exceeded');
throw new BpmnError('MAX_RETRIES_EXCEEDED',
`Operation failed after ${maxRetries} attempts: ${ex.message}`);
}
}

Error Recovery Workflow

Complete example with retry and fallback:

<bpmn:process id="ResilientAPICall" name="Resilient API Call">

<!-- Main API call -->
<bpmn:scriptTask id="CallPrimaryAPI" name="Call Primary API">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script"><![CDATA[
context.retryCount = (context.retryCount || 0) + 1;

var result = BankLingo.ExecuteCommand('CallAPI', {
endpoint: 'primary',
data: context.requestData
});

if (!result.success) {
throw new BpmnError('API_ERROR',
'Primary API failed: ' + result.error);
}

context.apiData = result.data;
context.retryCount = 0; // Reset on success
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:scriptTask>

<!-- Catch API errors -->
<bpmn:boundaryEvent id="APIFailed"
attachedToRef="CallPrimaryAPI"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="APIError" />
</bpmn:boundaryEvent>

<!-- Retry logic -->
<bpmn:scriptTask id="CheckRetry" name="Check Retry">
<bpmn:script>
if (context.retryCount < 3) {
logger.warn('Retry ' + context.retryCount + '/3');
// Flow back to CallPrimaryAPI
} else {
logger.error('Max retries exceeded, trying fallback');
// Flow to fallback
}
</bpmn:script>
</bpmn:scriptTask>

<!-- Fallback API -->
<bpmn:scriptTask id="CallFallbackAPI" name="Call Fallback API">
<bpmn:script>
logger.info('Using fallback API');

var result = BankLingo.ExecuteCommand('CallAPI', {
endpoint: 'fallback',
data: context.requestData
});

if (!result.success) {
throw new BpmnError('FALLBACK_FAILED',
'Both primary and fallback APIs failed');
}

context.apiData = result.data;
context.usedFallback = true;
</bpmn:script>
</bpmn:scriptTask>

<!-- Error definitions -->
<bpmn:error id="APIError" errorCode="API_ERROR" />
<bpmn:error id="FallbackError" errorCode="FALLBACK_FAILED" />
</bpmn:process>

Differences from Standard JavaScript Errors

Regular JavaScript Error ❌

// Standard JavaScript error (NOT caught by BPMN)
throw new Error('Something went wrong');

// Will fail the process without boundary event catching

BPMN Error ✅

// BPMN-aware error (caught by boundary events)
throw new BpmnError('ERROR_CODE', 'Error message');

// Can be caught, handled, and recovered from

Best Practices

✅ Do This

// ✅ Use specific error codes
throw new BpmnError('INSUFFICIENT_FUNDS', 'Balance too low');

// ✅ Include helpful context
throw new BpmnError('VALIDATION_ERROR',
`Invalid email format: ${context.email}`);

// ✅ Log before throwing
logger.error('Validation failed: missing customer ID');
throw new BpmnError('VALIDATION_ERROR', 'Customer ID required');

// ✅ Check conditions before operations
if (!context.apiKey) {
throw new BpmnError('CONFIGURATION_ERROR', 'API key missing');
}

❌ Don't Do This

// ❌ Generic error codes
throw new BpmnError('ERROR', 'Something failed');

// ❌ Empty or unclear messages
throw new BpmnError('VALIDATION_ERROR', '');

// ❌ Using standard Error
throw new Error('This will not be caught by BPMN');

// ❌ Not logging errors
throw new BpmnError('API_ERROR', 'Failed'); // No context logged

Phase: 5 - Error Handling
Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026