Error Handling Patterns
Error handling and recovery patterns in Aurelia 2, including component error management, event handling, and user-friendly error recovery strategies.
Aurelia 2 provides several mechanisms for handling errors gracefully and implementing recovery strategies. This guide covers the practical patterns for managing errors in components, events, and user interactions.
Component Error Handling
Promise-based Error Handling
Aurelia uses promise-based error handling throughout its lifecycle. Errors in lifecycle hooks are caught and propagated through promise chains:
export class ErrorHandlingComponent {
private data: any[] = [];
private error: Error | null = null;
private loading = false;
async binding(): Promise<void> {
this.loading = true;
this.error = null;
try {
this.data = await this.dataService.loadData();
} catch (error) {
this.error = error instanceof Error ? error : new Error('Unknown error');
console.error('Data loading failed:', error);
} finally {
this.loading = false;
}
}
async retryLoad(): Promise<void> {
await this.binding();
}
}
Template:
<template>
<div if.bind="loading">Loading...</div>
<div else-if.bind="error" class="error">
<p>Error: ${error.message}</p>
<button click.trigger="retryLoad()">Retry</button>
</div>
<div else>
<div repeat.for="item of data">${item.name}</div>
</div>
</template>
Lifecycle Hook Error Management
Handle errors in different lifecycle hooks with appropriate recovery strategies:
export class RobustComponent {
private initializationError: Error | null = null;
private bindingError: Error | null = null;
created(): void {
try {
this.initializeComponent();
} catch (error) {
this.initializationError = error instanceof Error ? error : new Error('Initialization failed');
console.error('Component initialization failed:', error);
}
}
async binding(): Promise<void> {
if (this.initializationError) {
// Skip binding if initialization failed
return;
}
try {
await this.bindData();
} catch (error) {
this.bindingError = error instanceof Error ? error : new Error('Binding failed');
console.error('Data binding failed:', error);
}
}
attached(): void {
if (this.initializationError || this.bindingError) {
// Show error state instead of normal functionality
this.showErrorState();
}
}
private initializeComponent(): void {
// Component initialization logic
}
private async bindData(): Promise<void> {
// Data binding logic
}
private showErrorState(): void {
// Show error UI
}
}
Event Handler Error Handling
Safe Event Handlers
Aurelia provides built-in error handling for event handlers. You can configure custom error handling:
import { ListenerBindingOptions } from 'aurelia';
export class EventErrorComponent {
private errorCount = 0;
private lastError: Error | null = null;
// Configure error handling for event listeners
private eventOptions = new ListenerBindingOptions(
false, // prevent
false, // capture
(event: Event, error: unknown) => {
this.handleEventError(event, error);
}
);
handleClick(): void {
// This might throw an error
throw new Error('Button click failed');
}
private handleEventError(event: Event, error: unknown): void {
this.errorCount++;
this.lastError = error instanceof Error ? error : new Error('Unknown event error');
console.error('Event handler error:', error);
// Show user-friendly message
if (this.errorCount > 3) {
this.showCriticalError();
}
}
private showCriticalError(): void {
// Show critical error UI
}
}
Error-Safe Event Handlers
Wrap event handlers in try-catch blocks for custom error handling:
export class SafeEventComponent {
private errorMessage: string | null = null;
safeHandler(action: () => void | Promise<void>): void {
try {
const result = action();
if (result instanceof Promise) {
result.catch(error => {
this.handleError(error);
});
}
} catch (error) {
this.handleError(error);
}
}
handleButtonClick(): void {
this.safeHandler(() => {
// Potentially dangerous operation
this.performRiskyOperation();
});
}
async handleAsyncAction(): Promise<void> {
this.safeHandler(async () => {
await this.performAsyncOperation();
});
}
private handleError(error: unknown): void {
this.errorMessage = error instanceof Error ? error.message : 'An error occurred';
// Clear error after 5 seconds
setTimeout(() => {
this.errorMessage = null;
}, 5000);
}
private performRiskyOperation(): void {
// Operation that might fail
}
private async performAsyncOperation(): Promise<void> {
// Async operation that might fail
}
}
Promise Template Controller Error Handling
Aurelia provides declarative error handling in templates using the promise template controller:
export class PromiseErrorComponent {
dataPromise: Promise<any[]>;
constructor() {
this.dataPromise = this.loadData();
}
private async loadData(): Promise<any[]> {
// Simulate potential failure
if (Math.random() > 0.5) {
throw new Error('Data loading failed');
}
return [{ id: 1, name: 'Item 1' }];
}
retryLoad(): void {
this.dataPromise = this.loadData();
}
}
Template with promise error handling:
<template>
<div promise.bind="dataPromise">
<div pending>Loading data...</div>
<div then.bind="data">
<div repeat.for="item of data">${item.name}</div>
</div>
<div catch.bind="error" class="error">
<p>Error: ${error.message}</p>
<button click.trigger="retryLoad()">Retry</button>
</div>
</div>
</template>
Error Recovery Strategies
Retry with Backoff
Implement retry logic with exponential backoff:
export class RetryComponent {
private maxRetries = 3;
private retryCount = 0;
private backoffDelay = 1000; // Start with 1 second
async performWithRetry<T>(operation: () => Promise<T>): Promise<T> {
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
const result = await operation();
this.retryCount = 0; // Reset on success
return result;
} catch (error) {
this.retryCount = attempt + 1;
if (attempt === this.maxRetries) {
throw error; // Final attempt failed
}
// Wait before retry with exponential backoff
await this.delay(this.backoffDelay * Math.pow(2, attempt));
}
}
throw new Error('Max retries exceeded');
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async loadData(): Promise<any[]> {
return this.performWithRetry(async () => {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
});
}
}
Graceful Degradation
Provide fallback functionality when primary features fail:
export class GracefulDegradationComponent {
private primaryFeatureAvailable = true;
private fallbackData: any[] = [];
async attached(): Promise<void> {
try {
await this.initializePrimaryFeature();
} catch (error) {
console.warn('Primary feature unavailable, using fallback:', error);
this.primaryFeatureAvailable = false;
this.setupFallback();
}
}
private async initializePrimaryFeature(): Promise<void> {
// Try to initialize advanced feature
if (!this.hasRequiredCapabilities()) {
throw new Error('Required capabilities not available');
}
// Initialize primary feature
}
private hasRequiredCapabilities(): boolean {
// Check for required browser features, APIs, etc.
return 'fetch' in window && 'Promise' in window;
}
private setupFallback(): void {
// Setup simpler fallback functionality
this.fallbackData = [
{ id: 1, name: 'Fallback Item 1' },
{ id: 2, name: 'Fallback Item 2' }
];
}
handleAction(): void {
if (this.primaryFeatureAvailable) {
this.handlePrimaryAction();
} else {
this.handleFallbackAction();
}
}
private handlePrimaryAction(): void {
// Primary action implementation
}
private handleFallbackAction(): void {
// Fallback action implementation
}
}
User-Friendly Error Messaging
Error Message Component
Create a reusable error message component:
export class ErrorMessageComponent {
@bindable message: string = '';
@bindable type: 'error' | 'warning' | 'info' = 'error';
@bindable dismissible: boolean = true;
@bindable autoHide: boolean = false;
@bindable hideDelay: number = 5000;
private hideTimer?: number;
messageChanged(): void {
if (this.autoHide && this.message) {
this.startHideTimer();
}
}
dismiss(): void {
this.message = '';
this.clearHideTimer();
}
private startHideTimer(): void {
this.clearHideTimer();
this.hideTimer = window.setTimeout(() => {
this.dismiss();
}, this.hideDelay);
}
private clearHideTimer(): void {
if (this.hideTimer) {
clearTimeout(this.hideTimer);
this.hideTimer = undefined;
}
}
detaching(): void {
this.clearHideTimer();
}
}
Template:
<template>
<div if.bind="message" class="alert alert-${type}">
<span>${message}</span>
<button if.bind="dismissible" click.trigger="dismiss()" class="close">×</button>
</div>
</template>
Global Error Handler
Create a global error handler service:
import { singleton, IEventAggregator } from 'aurelia';
interface ErrorInfo {
message: string;
type: 'error' | 'warning' | 'info';
timestamp: Date;
id: string;
}
@singleton
export class ErrorHandlerService {
private errors: ErrorInfo[] = [];
private maxErrors = 10;
constructor(private eventAggregator: IEventAggregator) {}
handleError(error: Error | string, type: 'error' | 'warning' | 'info' = 'error'): void {
const errorInfo: ErrorInfo = {
message: error instanceof Error ? error.message : error,
type,
timestamp: new Date(),
id: this.generateId()
};
this.errors.unshift(errorInfo);
// Keep only the latest errors
if (this.errors.length > this.maxErrors) {
this.errors = this.errors.slice(0, this.maxErrors);
}
// Publish error event
this.eventAggregator.publish('error:occurred', errorInfo);
}
getRecentErrors(): ErrorInfo[] {
return [...this.errors];
}
clearErrors(): void {
this.errors = [];
}
private generateId(): string {
return Math.random().toString(36).substr(2, 9);
}
}
Testing Error Scenarios
Unit Testing Error Handling
import { TestContext } from '@aurelia/testing';
import { ErrorHandlingComponent } from './error-handling-component';
describe('ErrorHandlingComponent', () => {
let ctx: TestContext;
beforeEach(() => {
ctx = TestContext.create();
});
afterEach(() => {
ctx.dispose();
});
it('should handle binding errors gracefully', async () => {
// Mock service to throw error
const mockService = {
loadData: jest.fn().mockRejectedValue(new Error('Service unavailable'))
};
ctx.container.register(IDataService, mockService);
const { component, startPromise, tearDown } = ctx.createFixture(
'<error-handling-component></error-handling-component>',
ErrorHandlingComponent
);
await startPromise;
const viewModel = component.controller.viewModel;
expect(viewModel.error).toBeTruthy();
expect(viewModel.error.message).toBe('Service unavailable');
await tearDown();
});
it('should allow error recovery', async () => {
const mockService = {
loadData: jest.fn()
.mockRejectedValueOnce(new Error('Service unavailable'))
.mockResolvedValueOnce([{ id: 1, name: 'Item 1' }])
};
ctx.container.register(IDataService, mockService);
const { component, startPromise, tearDown } = ctx.createFixture(
'<error-handling-component></error-handling-component>',
ErrorHandlingComponent
);
await startPromise;
const viewModel = component.controller.viewModel;
expect(viewModel.error).toBeTruthy();
// Retry the operation
await viewModel.retryLoad();
expect(viewModel.error).toBeFalsy();
expect(viewModel.data).toHaveLength(1);
await tearDown();
});
});
Best Practices
1. Fail Fast, Recover Gracefully
Detect errors early in the component lifecycle
Provide user-friendly error messages
Implement retry mechanisms where appropriate
2. Error Boundary Pattern
Use promise-based error handling for async operations
Implement fallback UI for failed components
Prevent error propagation to parent components
3. Logging and Monitoring
Log errors for debugging and monitoring
Include context information (user actions, component state)
Use structured error codes for better categorization
4. User Experience
Show loading states during error recovery
Provide clear retry options
Use progressive disclosure for error details
5. Testing
Test both success and failure scenarios
Mock services to simulate various error conditions
Verify error recovery mechanisms work correctly
By implementing these error handling patterns, your Aurelia applications will be more robust, user-friendly, and maintainable, providing better experiences even when things go wrong.
Last updated
Was this helpful?