Error Handling¶
Implement robust error handling and recovery strategies in your lex-helper chatbots. This guide covers exception patterns, graceful degradation, and maintaining excellent user experience even when things go wrong.
Overview¶
Error handling in conversational AI is critical for user experience. Users should never see technical error messages or be left confused when something goes wrong. lex-helper provides comprehensive error handling capabilities that help you build resilient chatbots.
Exception Hierarchy¶
Built-in Exception Types¶
lex-helper defines a hierarchy of exceptions for different error scenarios:
from lex_helper.exceptions import (
LexError, # Base exception
IntentNotFoundError, # Intent handler not found
ValidationError, # Input validation failed
SessionError # Session state issues
)
# Base exception with error codes
class LexError(Exception):
def __init__(self, message: str, error_code: str | None = None):
super().__init__(message)
self.error_code = error_code
# Specific exception types
class IntentNotFoundError(LexError):
"""Raised when an intent handler cannot be found."""
pass
class ValidationError(LexError):
"""Raised when input validation fails."""
pass
class SessionError(LexError):
"""Raised when there's an issue with the session state."""
pass
Custom Exception Types¶
Create domain-specific exceptions for your chatbot:
from lex_helper.exceptions import LexError
class BookingError(LexError):
"""Base class for booking-related errors."""
pass
class FlightNotFoundError(BookingError):
"""Raised when requested flight is not available."""
pass
class PaymentError(BookingError):
"""Raised when payment processing fails."""
pass
class ExternalServiceError(LexError):
"""Raised when external API calls fail."""
def __init__(self, service_name: str, message: str, status_code: int = None):
super().__init__(f"{service_name} error: {message}")
self.service_name = service_name
self.status_code = status_code
Automatic Error Handling¶
Configuration¶
Enable automatic error handling in your LexHelper configuration:
from lex_helper import LexHelper, Config
config = Config(
session_attributes=MySessionAttributes(),
auto_handle_exceptions=True, # Enable automatic error handling
error_message="I'm sorry, something went wrong. Please try again."
)
lex_helper = LexHelper(config)
How Automatic Handling Works¶
When enabled, lex-helper automatically:
- Catches all exceptions in your intent handlers
- Logs the error for debugging
- Preserves session state so users don't lose context
- Returns user-friendly messages instead of technical errors
- Maintains conversation flow by closing gracefully
def handler(lex_request: LexRequest[MySessionAttributes]) -> LexResponse[MySessionAttributes]:
# This will be automatically caught if it fails
result = external_api_call()
# If an exception occurs, user sees the configured error_message
# instead of a technical stack trace
return process_result(result)
Custom Error Messages¶
Provide specific error messages or use message keys for internationalization:
# Direct error message
config = Config(
auto_handle_exceptions=True,
error_message="We're experiencing technical difficulties. Please try again in a few minutes."
)
# Message key (requires MessageManager)
config = Config(
auto_handle_exceptions=True,
error_message="error.technical_difficulty" # Looks up localized message
)
Manual Error Handling¶
Using handle_exceptions¶
For more control, use the handle_exceptions function directly:
from lex_helper.exceptions import handle_exceptions
def handler(lex_request: LexRequest[MySessionAttributes]) -> LexResponse[MySessionAttributes]:
try:
return process_booking_request(lex_request)
except FlightNotFoundError as e:
# Handle specific error with custom message
return handle_exceptions(
e,
lex_request,
error_message="I couldn't find flights for those dates. Let's try different dates."
)
except PaymentError as e:
# Handle payment errors differently
return handle_exceptions(
e,
lex_request,
error_message="There was an issue processing your payment. Please check your payment method."
)
except Exception as e:
# Catch-all for unexpected errors
return handle_exceptions(e, lex_request)
Exception-Specific Messages¶
The handle_exceptions function provides default messages for different exception types:
# These exceptions get specific default messages:
IntentNotFoundError → "I'm not sure how to handle that request."
ValidationError → "Invalid input provided." (or the exception message)
SessionError → "There was an issue with your session. Please start over."
ValueError → "Invalid value provided."
# Other exceptions get a generic message
Exception → "I'm sorry, I encountered an error while processing your request."
Error Recovery Patterns¶
Retry with Backoff¶
Implement retry logic for transient failures:
import time
from typing import Optional
def handler(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
session_attrs = lex_request.sessionState.sessionAttributes
# Track retry attempts
retry_count = getattr(session_attrs, 'api_retry_count', 0)
try:
result = call_external_api()
# Reset retry count on success
session_attrs.api_retry_count = 0
return process_successful_result(result, lex_request)
except ExternalServiceError as e:
if retry_count < 3:
# Increment retry count and try again
session_attrs.api_retry_count = retry_count + 1
# Exponential backoff
time.sleep(2 ** retry_count)
return dialog.elicit_intent(
messages=[LexPlainText(content="Let me try that again...")],
lex_request=lex_request
)
else:
# Max retries reached
session_attrs.api_retry_count = 0
return handle_exceptions(
e,
lex_request,
error_message="I'm having trouble connecting to our booking system. Please try again later."
)
Graceful Degradation¶
Provide alternative functionality when primary features fail:
def handler(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
try:
# Try primary booking flow
return book_flight_online(lex_request)
except ExternalServiceError:
# Fall back to offline booking
return offer_offline_booking(lex_request)
def offer_offline_booking(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
"""Offer alternative when online booking fails."""
session_attrs = lex_request.sessionState.sessionAttributes
# Store user's request for later processing
session_attrs.offline_booking_request = {
"departure": session_attrs.departure_city,
"arrival": session_attrs.arrival_city,
"date": session_attrs.travel_date
}
return dialog.close(
messages=[
LexPlainText(
content="Our booking system is temporarily unavailable. "
"I've saved your request and our team will contact you within 2 hours to complete your booking."
)
],
lex_request=lex_request
)
Context Preservation¶
Maintain conversation context even when errors occur:
def handler(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
session_attrs = lex_request.sessionState.sessionAttributes
try:
return process_payment(lex_request)
except PaymentError as e:
# Preserve booking details even when payment fails
session_attrs.payment_failed = True
session_attrs.payment_error_reason = str(e)
# Offer alternative payment methods
return dialog.elicit_slot(
slot_to_elicit="PaymentMethod",
messages=[
LexPlainText(content="That payment method didn't work. Would you like to try a different card?"),
LexImageResponseCard(
title="Payment Options",
buttons=[
{"text": "Try Different Card", "value": "different_card"},
{"text": "PayPal", "value": "paypal"},
{"text": "Call to Pay", "value": "phone_payment"}
]
)
],
lex_request=lex_request
)
Validation Error Handling¶
Input Validation¶
Validate user input and provide helpful feedback:
import re
from datetime import datetime, timedelta
def validate_email(email: str) -> tuple[bool, str]:
"""Validate email format."""
if not email:
return False, "Please provide an email address."
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email):
return False, "Please provide a valid email address like user@example.com."
return True, ""
def validate_travel_date(date_str: str) -> tuple[bool, str]:
"""Validate travel date."""
try:
travel_date = datetime.fromisoformat(date_str)
now = datetime.now()
if travel_date < now:
return False, "Travel date cannot be in the past. When would you like to travel?"
if travel_date > now + timedelta(days=365):
return False, "We can only book flights up to one year in advance."
return True, ""
except ValueError:
return False, "I didn't understand that date. Please use a format like 'March 15' or '2024-03-15'."
def handler(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
intent = lex_request.sessionState.intent
# Validate email if provided
email = dialog.get_slot("Email", intent)
if email:
is_valid, error_message = validate_email(email)
if not is_valid:
# Clear invalid slot and re-prompt
dialog.set_slot("Email", None, intent)
return dialog.elicit_slot(
slot_to_elicit="Email",
messages=[LexPlainText(content=error_message)],
lex_request=lex_request
)
# Continue with valid input
return process_booking(lex_request)
Session Attribute Validation¶
Handle Pydantic validation errors gracefully:
from pydantic import ValidationError
def handler(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
try:
session_attrs = lex_request.sessionState.sessionAttributes
# This might trigger Pydantic validation
session_attrs.passenger_count = int(user_input)
session_attrs.email = user_email
return continue_booking_flow(lex_request)
except ValidationError as e:
# Convert Pydantic errors to user-friendly messages
error_messages = []
for error in e.errors():
field = error['loc'][0] if error['loc'] else 'input'
message = error['msg']
# Customize messages for better UX
if 'email' in field.lower():
error_messages.append("Please provide a valid email address.")
elif 'passenger_count' in field.lower():
error_messages.append("Passenger count must be between 1 and 9.")
else:
error_messages.append(f"{field}: {message}")
return dialog.elicit_intent(
messages=[LexPlainText(content=f"Please correct: {', '.join(error_messages)}")],
lex_request=lex_request
)
Logging and Monitoring¶
Structured Logging¶
Implement comprehensive logging for error tracking:
import logging
import json
from datetime import datetime
# Configure structured logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def handler(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
session_id = lex_request.sessionId
intent_name = lex_request.sessionState.intent.name
try:
logger.info(
"Processing intent",
extra={
"session_id": session_id,
"intent": intent_name,
"user_input": lex_request.inputTranscript
}
)
result = process_booking_request(lex_request)
logger.info(
"Intent processed successfully",
extra={
"session_id": session_id,
"intent": intent_name,
"response_type": result.sessionState.dialogAction.type
}
)
return result
except Exception as e:
logger.error(
"Intent processing failed",
extra={
"session_id": session_id,
"intent": intent_name,
"error_type": type(e).__name__,
"error_message": str(e),
"user_input": lex_request.inputTranscript
},
exc_info=True
)
return handle_exceptions(e, lex_request)
Error Metrics¶
Track error rates and patterns:
import boto3
from datetime import datetime
cloudwatch = boto3.client('cloudwatch')
def log_error_metric(error_type: str, intent_name: str):
"""Log error metrics to CloudWatch."""
try:
cloudwatch.put_metric_data(
Namespace='Chatbot/Errors',
MetricData=[
{
'MetricName': 'ErrorCount',
'Dimensions': [
{'Name': 'ErrorType', 'Value': error_type},
{'Name': 'Intent', 'Value': intent_name}
],
'Value': 1,
'Unit': 'Count',
'Timestamp': datetime.utcnow()
}
]
)
except Exception:
# Don't let metric logging break the main flow
logger.exception("Failed to log error metric")
def handler(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
intent_name = lex_request.sessionState.intent.name
try:
return process_booking_request(lex_request)
except ValidationError as e:
log_error_metric("ValidationError", intent_name)
return handle_exceptions(e, lex_request)
except ExternalServiceError as e:
log_error_metric("ExternalServiceError", intent_name)
return handle_exceptions(e, lex_request)
except Exception as e:
log_error_metric("UnexpectedError", intent_name)
return handle_exceptions(e, lex_request)
Testing Error Scenarios¶
Unit Testing Error Handling¶
Test your error handling logic thoroughly:
import pytest
from unittest.mock import Mock, patch
from lex_helper.exceptions import ValidationError, ExternalServiceError
def test_handler_validates_email():
"""Test that handler validates email input."""
# Arrange
lex_request = create_test_request(slots={"Email": {"value": {"interpretedValue": "invalid-email"}}})
# Act
response = handler(lex_request)
# Assert
assert response.sessionState.dialogAction.type == "ElicitSlot"
assert response.sessionState.dialogAction.slotToElicit == "Email"
assert "valid email" in response.messages[0].content.lower()
def test_handler_retries_on_service_error():
"""Test that handler retries on external service errors."""
# Arrange
lex_request = create_test_request()
with patch('my_chatbot.intents.book_flight.call_external_api') as mock_api:
# First call fails, second succeeds
mock_api.side_effect = [ExternalServiceError("API", "Timeout"), {"success": True}]
# Act
response = handler(lex_request)
# Assert
assert mock_api.call_count == 2
assert response.sessionState.dialogAction.type == "Close"
def test_handler_handles_max_retries():
"""Test that handler gives up after max retries."""
# Arrange
lex_request = create_test_request()
lex_request.sessionState.sessionAttributes.api_retry_count = 3
with patch('my_chatbot.intents.book_flight.call_external_api') as mock_api:
mock_api.side_effect = ExternalServiceError("API", "Persistent failure")
# Act
response = handler(lex_request)
# Assert
assert "try again later" in response.messages[0].content.lower()
assert response.sessionState.sessionAttributes.api_retry_count == 0
Integration Testing¶
Test error scenarios with realistic conditions:
def test_error_handling_integration():
"""Test error handling with full request flow."""
# Create realistic error scenario
with patch('requests.get') as mock_get:
mock_get.side_effect = requests.exceptions.Timeout()
# Process request
event = load_test_event('book_flight_complete.json')
response = lambda_handler(event, {})
# Verify graceful error handling
assert response['sessionState']['dialogAction']['type'] == 'Close'
assert 'try again' in response['messages'][0]['content']
Best Practices¶
1. Fail Gracefully¶
Always provide a path forward for users:
def handler(lex_request: LexRequest[MySessionAttributes]) -> LexResponse[MySessionAttributes]:
try:
return process_primary_flow(lex_request)
except Exception:
# Always provide an alternative
return offer_alternative_or_escalation(lex_request)
2. Preserve User Context¶
Don't lose user progress when errors occur:
def handler(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
session_attrs = lex_request.sessionState.sessionAttributes
try:
return complete_booking(lex_request)
except PaymentError:
# Keep booking details, just retry payment
session_attrs.booking_ready = True
return retry_payment_flow(lex_request)
3. Use Specific Error Messages¶
Provide actionable guidance in error messages:
# Good: Specific and actionable
"Please provide a valid email address like user@example.com"
# Avoid: Vague and unhelpful
"Invalid input"
4. Log Errors Appropriately¶
Log enough information for debugging without exposing sensitive data:
logger.error(
"Booking failed",
extra={
"session_id": lex_request.sessionId,
"intent": intent_name,
"error_type": type(e).__name__,
# Don't log sensitive data like credit card numbers
"booking_details": {
"departure": session_attrs.departure_city,
"arrival": session_attrs.arrival_city
}
}
)
5. Test Error Scenarios¶
Include error scenarios in your test suite:
def test_all_error_paths():
"""Test various error conditions."""
test_cases = [
(ValidationError("Invalid email"), "valid email"),
(ExternalServiceError("API", "Timeout"), "try again"),
(PaymentError("Card declined"), "payment method")
]
for error, expected_message in test_cases:
with patch('handler.process_request', side_effect=error):
response = handler(create_test_request())
assert expected_message in response.messages[0].content.lower()
Advanced Error Handling¶
Circuit Breaker Pattern¶
Prevent cascading failures with circuit breakers:
from datetime import datetime, timedelta
from enum import Enum
class CircuitState(Enum):
CLOSED = "closed" # Normal operation
OPEN = "open" # Failing, reject requests
HALF_OPEN = "half_open" # Testing if service recovered
class CircuitBreaker:
def __init__(self, failure_threshold: int = 5, timeout: int = 60):
self.failure_threshold = failure_threshold
self.timeout = timeout
self.failure_count = 0
self.last_failure_time = None
self.state = CircuitState.CLOSED
def call(self, func, *args, **kwargs):
if self.state == CircuitState.OPEN:
if self._should_attempt_reset():
self.state = CircuitState.HALF_OPEN
else:
raise ExternalServiceError("Circuit breaker", "Service unavailable")
try:
result = func(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure()
raise e
def _should_attempt_reset(self) -> bool:
return (
self.last_failure_time and
datetime.now() - self.last_failure_time > timedelta(seconds=self.timeout)
)
def _on_success(self):
self.failure_count = 0
self.state = CircuitState.CLOSED
def _on_failure(self):
self.failure_count += 1
self.last_failure_time = datetime.now()
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
# Usage
flight_api_circuit = CircuitBreaker(failure_threshold=3, timeout=30)
def handler(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
try:
flights = flight_api_circuit.call(search_flights, departure, arrival, date)
return present_flight_options(flights, lex_request)
except ExternalServiceError:
return offer_callback_booking(lex_request)
Error Recovery Workflows¶
Implement sophisticated error recovery:
def handler(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
session_attrs = lex_request.sessionState.sessionAttributes
# Check if we're in error recovery mode
if session_attrs.error_recovery_active:
return handle_error_recovery(lex_request)
try:
return normal_booking_flow(lex_request)
except Exception as e:
# Enter error recovery mode
session_attrs.error_recovery_active = True
session_attrs.original_error = str(e)
session_attrs.recovery_options = determine_recovery_options(e)
return present_recovery_options(lex_request)
def handle_error_recovery(lex_request: LexRequest[BookingSessionAttributes]) -> LexResponse[BookingSessionAttributes]:
"""Handle user's choice of recovery option."""
session_attrs = lex_request.sessionState.sessionAttributes
intent = lex_request.sessionState.intent
recovery_choice = dialog.get_slot("RecoveryChoice", intent)
if recovery_choice == "retry":
# Clear error state and retry
session_attrs.error_recovery_active = False
return normal_booking_flow(lex_request)
elif recovery_choice == "alternative":
# Offer alternative booking method
return offer_phone_booking(lex_request)
elif recovery_choice == "later":
# Save progress and exit gracefully
return save_progress_and_exit(lex_request)
Related Topics¶
- Core Concepts - Understanding lex-helper architecture
- Session Attributes - Managing conversation state safely
- Intent Handling - Implementing robust intent handlers
- API Reference - Complete exception handling API
- Advanced Topics - Advanced debugging techniques
Next: Explore Smart Disambiguation for AI-powered intent resolution.