Skip to content

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