Error Handling¶
Standards for handling errors and exceptions in the Odoo 15 system.
Overview¶
Proper error handling improves user experience, aids debugging, and prevents data corruption.
Odoo Exception Types¶
Built-in Exceptions¶
| Exception | Use Case | User Sees |
|---|---|---|
UserError |
User-caused errors | Friendly message |
ValidationError |
Data validation failures | Validation message |
AccessError |
Permission denied | Access denied message |
AccessDenied |
Authentication failure | Login prompt |
MissingError |
Record not found | Record missing message |
RedirectWarning |
Redirect with message | Warning + redirect |
Import Exceptions¶
from odoo.exceptions import (
UserError,
ValidationError,
AccessError,
AccessDenied,
MissingError,
RedirectWarning,
)
When to Use Each Exception¶
UserError¶
For user-facing errors that require action.
from odoo.exceptions import UserError
def action_confirm(self):
if not self.order_line:
raise UserError("Cannot confirm order without order lines.")
if self.amount_total <= 0:
raise UserError("Order total must be greater than zero.")
ValidationError¶
For constraint violations and data validation.
from odoo.exceptions import ValidationError
@api.constrains('email')
def _check_email(self):
for record in self:
if record.email and '@' not in record.email:
raise ValidationError("Please enter a valid email address.")
@api.constrains('quantity')
def _check_quantity(self):
for record in self:
if record.quantity < 0:
raise ValidationError("Quantity cannot be negative.")
AccessError¶
For permission-related errors.
from odoo.exceptions import AccessError
def action_approve(self):
if not self.env.user.has_group('my_module.group_manager'):
raise AccessError("Only managers can approve orders.")
MissingError¶
When a record doesn't exist.
from odoo.exceptions import MissingError
def get_partner(self, partner_id):
partner = self.env['res.partner'].browse(partner_id)
if not partner.exists():
raise MissingError(f"Partner with ID {partner_id} not found.")
return partner
RedirectWarning¶
When you want to show a message and redirect.
from odoo.exceptions import RedirectWarning
def check_configuration(self):
api_key = self.env['ir.config_parameter'].sudo().get_param('api.key')
if not api_key:
action = self.env.ref('base_setup.action_general_configuration')
raise RedirectWarning(
"API key not configured. Please configure it in settings.",
action.id,
"Go to Settings"
)
Error Handling Patterns¶
Try-Except-Finally¶
def process_order(self):
try:
self._validate_order()
self._process_payment()
self._send_confirmation()
except PaymentError as e:
_logger.error(f"Payment failed: {e}")
raise UserError(f"Payment failed: {e}")
except Exception as e:
_logger.exception(f"Unexpected error: {e}")
raise
finally:
self._cleanup_temp_files()
Graceful Degradation¶
def get_customer_info(self):
try:
# Try external API
return self._fetch_from_api()
except APIError:
_logger.warning("API unavailable, using cached data")
return self._get_cached_data()
except Exception:
_logger.exception("Failed to get customer info")
return {} # Return empty dict instead of crashing
Retry Pattern¶
import time
def call_api_with_retry(self, max_retries=3):
for attempt in range(max_retries):
try:
return self._make_api_call()
except TemporaryError as e:
if attempt < max_retries - 1:
wait_time = 2 ** attempt # Exponential backoff
_logger.warning(f"Attempt {attempt + 1} failed, retrying in {wait_time}s")
time.sleep(wait_time)
else:
_logger.error(f"All {max_retries} attempts failed")
raise UserError("Service temporarily unavailable. Please try again later.")
Transaction Handling¶
def batch_update(self, records):
success = []
failed = []
for record in records:
try:
with self.env.cr.savepoint():
record._process()
success.append(record.id)
except Exception as e:
_logger.warning(f"Record {record.id} failed: {e}")
failed.append((record.id, str(e)))
if failed:
_logger.error(f"Batch update: {len(failed)}/{len(records)} failed")
return {'success': success, 'failed': failed}
User-Friendly Messages¶
Good Messages¶
# Clear, actionable messages
raise UserError("Please enter a valid phone number (e.g., +1-555-123-4567).")
raise UserError("Order cannot be confirmed because the customer has exceeded their credit limit.")
raise ValidationError("The end date must be after the start date.")
Bad Messages¶
# Too technical
raise UserError(f"NoneType object has no attribute 'name'")
raise UserError(f"IntegrityError: duplicate key value violates unique constraint")
# Too vague
raise UserError("Error")
raise UserError("Something went wrong")
# Blaming the user
raise UserError("You made an error!")
Message Guidelines¶
| Do | Don't |
|---|---|
| Explain what went wrong | Show stack traces |
| Suggest how to fix it | Use technical jargon |
| Be specific | Be vague |
| Be polite | Blame the user |
| Use complete sentences | Use error codes only |
Logging Errors¶
Log and Raise¶
def risky_operation(self):
try:
self._do_something_risky()
except SpecificError as e:
_logger.error(f"Specific error in risky_operation: {e}")
raise UserError("Operation failed. Please contact support.")
except Exception as e:
# Log full traceback for unexpected errors
_logger.exception(f"Unexpected error in risky_operation")
raise
Exception Logging¶
# Log with traceback
_logger.exception("Error message") # Automatically includes traceback
# Log without traceback
_logger.error(f"Error: {e}")
# Log with custom traceback
import traceback
_logger.error(f"Error: {e}\n{traceback.format_exc()}")
API Error Handling¶
REST API Errors¶
from odoo.http import request, Response
import json
class MyController(http.Controller):
@http.route('/api/orders', type='json', auth='api_key')
def get_orders(self):
try:
orders = request.env['sale.order'].search([])
return {'status': 'success', 'data': orders.read(['name', 'amount_total'])}
except AccessError:
return {'status': 'error', 'message': 'Permission denied', 'code': 403}
except Exception as e:
_logger.exception("API error in get_orders")
return {'status': 'error', 'message': 'Internal server error', 'code': 500}
Error Response Format¶
def _error_response(self, message, code, details=None):
return {
'status': 'error',
'code': code,
'message': message,
'details': details,
'timestamp': fields.Datetime.now().isoformat()
}
# Usage
return self._error_response(
message="Validation failed",
code=400,
details={'field': 'email', 'error': 'Invalid format'}
)
Form Validation¶
Multiple Errors¶
@api.constrains('start_date', 'end_date', 'quantity', 'price')
def _validate_fields(self):
errors = []
for record in self:
if record.start_date and record.end_date:
if record.end_date < record.start_date:
errors.append("End date must be after start date.")
if record.quantity <= 0:
errors.append("Quantity must be greater than zero.")
if record.price < 0:
errors.append("Price cannot be negative.")
if errors:
raise ValidationError("\n".join(errors))
Field-Specific Validation¶
@api.constrains('email')
def _check_email(self):
for record in self:
if record.email:
if '@' not in record.email:
raise ValidationError(_("Email: Invalid email format"))
if self.search_count([('email', '=', record.email), ('id', '!=', record.id)]):
raise ValidationError(_("Email: This email is already in use"))
External Service Errors¶
API Integration¶
import requests
from requests.exceptions import Timeout, ConnectionError, HTTPError
def call_external_service(self, data):
try:
response = requests.post(
'https://api.service.com/endpoint',
json=data,
timeout=30
)
response.raise_for_status()
return response.json()
except Timeout:
_logger.error("External service timeout")
raise UserError("The external service is taking too long. Please try again.")
except ConnectionError:
_logger.error("Cannot connect to external service")
raise UserError("Cannot connect to the service. Please check your internet connection.")
except HTTPError as e:
if e.response.status_code == 401:
_logger.error("Authentication failed with external service")
raise UserError("Authentication with the service failed. Please check your credentials.")
elif e.response.status_code == 429:
_logger.warning("Rate limit exceeded")
raise UserError("Too many requests. Please wait a moment and try again.")
else:
_logger.error(f"HTTP error: {e.response.status_code}")
raise UserError(f"Service error: {e.response.status_code}")
except Exception as e:
_logger.exception("Unexpected error calling external service")
raise UserError("An unexpected error occurred. Please try again later.")
Error Recovery¶
Cleanup on Error¶
def create_with_cleanup(self, vals):
temp_file = None
try:
temp_file = self._create_temp_file(vals)
record = self.create(vals)
self._upload_file(record, temp_file)
return record
except Exception:
_logger.exception("Error during create_with_cleanup")
raise
finally:
if temp_file and os.path.exists(temp_file):
os.remove(temp_file)
Partial Success Handling¶
def send_notifications(self, recipients):
results = {'sent': [], 'failed': []}
for recipient in recipients:
try:
self._send_notification(recipient)
results['sent'].append(recipient.id)
except Exception as e:
_logger.warning(f"Failed to notify {recipient.id}: {e}")
results['failed'].append({'id': recipient.id, 'error': str(e)})
if results['failed']:
failed_count = len(results['failed'])
total_count = len(recipients)
if failed_count == total_count:
raise UserError("Failed to send any notifications.")
else:
# Partial success - return results instead of raising
return results
return results
Quick Reference¶
Exception Selection¶
User did something wrong? → UserError
Data validation failed? → ValidationError
Permission denied? → AccessError
Record doesn't exist? → MissingError
Need to redirect user? → RedirectWarning
Programming error? → Let it raise naturally
Template¶
from odoo import models, api
from odoo.exceptions import UserError, ValidationError
import logging
_logger = logging.getLogger(__name__)
class MyModel(models.Model):
_name = 'my.model'
def my_method(self):
try:
# Validate first
if not self._is_valid():
raise UserError("Invalid state for this operation.")
# Do the work
result = self._do_work()
return result
except UserError:
raise # Re-raise user errors as-is
except Exception as e:
_logger.exception(f"Error in my_method for {self.id}")
raise UserError("An error occurred. Please contact support.")
Checklist¶
- Using appropriate exception type
- User-friendly error messages
- Logging errors appropriately
- Not exposing internal details
- Cleaning up resources on error
- Handling partial failures
- Documenting expected exceptions