Quick Answer
Stop silent failures. Bad error handling: Cron job hits API timeout, catches with except: pass, 50 orders marked "Shipped" but never reached warehouse. Result: $5,000 in refunds, 2-week delays. Production-ready handling: Catches specific error, logs order IDs, triggers "Sync Failed" flag, emails ops manager. Result: 5-minute alert, retry sync, on-time shipping. Systems will fail—how they fail defines reliability.
The Error Handling Problem
Your D2C brand integrates with a 3PL for shipping. A cron job runs every hour to push orders.
Scenario A (Bad Error Handling)
The cron job hits a timeout error from the 3PL API. The code catches the exception with except: pass to "keep things running."
Result: 50 orders are marked "Shipped" in Odoo but never actually reached the warehouse. Customers wait 2 weeks. You lose $5,000 in refunds and reputation. You only find out when customers start screaming.
Scenario B (Production-Ready Handling)
The API times out. The code catches the specific connection error, logs the exact order IDs and error response to the Odoo log file, triggers a "Sync Failed" flag on the order, and sends an email to the ops manager.
Result: Ops manager sees the alert in 5 minutes. Retries the sync. Packages ship on time.
We've implemented 150+ Odoo systems. The difference between a stable enterprise system and a fragile prototype is how it fails. Systems will fail. APIs go down. Data gets messy. The code that handles those failures defines your system's reliability.
The Odoo Exception Hierarchy
1. UserError (Logic Blocking)
Stops execution and shows a warning pop-up to the user. Does NOT log a traceback (because it's a known logic stop).
Use when: The user tries to do something invalid (e.g., ship an order with no address).
from odoo.exceptions import UserError
def action_ship(self):
for order in self:
if not order.partner_shipping_id:
raise UserError(f"Cannot ship Order {order.name}: No shipping address defined.")
2. ValidationError (Data Integrity)
Stops execution. Used primarily in @api.constrains. Ensures data entering the database is valid.
Use when: Data format is wrong (e.g., negative price).
from odoo.exceptions import ValidationError
@api.constrains('price')
def _check_price(self):
if self.price < 0:
raise ValidationError("Price cannot be negative.")
3. AccessError (Security)
Stops execution. Used when a user tries to touch a record they shouldn't.
Use when: Enforcing custom security rules in Python.
4. RedirectWarning (Recoverable Error)
Shows a warning with a button to fix the issue (e.g., "No carrier selected. Click here to configure.").
Logging: The Eyes and Ears of Your System
Using print() in production is a crime. It gets lost in the noise and provides no context. Use the Python logging module.
Setup
import logging
_logger = logging.getLogger(__name__) # Sets logger name to current module
Logging Levels & When to Use Them
| Level | When to Use | Example |
|---|---|---|
| DEBUG | Dev only, variable values | _logger.debug("Processing order %s", order.id) |
| INFO | Successful actions | _logger.info("Order %s synced", order.name) |
| WARNING | Unexpected but handled | _logger.warning("Skipping order %s: no email", order.name) |
| ERROR | Something broke, logged | _logger.error("Sync failed for %s", order.name) |
| CRITICAL | Money lost, data corrupted | _logger.critical("Payment taken, DB failed") |
The Golden Rule: Always Include Context
"Error occurred" is useless. "Error syncing Order #1005: Connection Timeout" is actionable.
try:
api.push_order(order.id)
except Exception as e:
# BAD: _logger.error("It failed")
# GOOD: Include ID, Name, and the Stack Trace
_logger.error(
"Failed to push Order %s (%s) to 3PL. Error: %s",
order.name, order.id, str(e), exc_info=True
)
# exc_info=True adds the python traceback to the log
Transaction Management: Rolling Back Safely
Odoo wraps every request in a database transaction. If an error is raised (and not caught), the entire transaction rolls back. This is usually good (prevents half-saved data).
The Trap
If you are looping through 1,000 orders to sync them, and Order #999 throws an error, ALL 999 previous orders roll back if you don't handle it.
Pattern: The "Safe Loop"
def action_batch_sync(self):
orders = self.search([('state', '=', 'sale'), ('is_synced', '=', False)])
for order in orders:
# Create a "savepoint" before processing each record
try:
with self.env.cr.savepoint():
self._sync_single_order(order)
# If successful, this commits locally within the transaction
except UserError as e:
# Logic error: Log warning, don't crash batch
_logger.warning("Skipping order %s: %s", order.name, str(e))
except Exception as e:
# System error: Log error, don't crash batch
_logger.error("Crash on order %s: %s", order.name, str(e), exc_info=True)
Why savepoint() matters: It isolates the transaction for that specific iteration. If _sync_single_order crashes, only that order's changes are rolled back. The loop continues to the next order.
Real-World: Payment Gateway Integration
You are capturing payments. This involves an external API call.
The Risk:
The API charges the card, but Odoo crashes while saving the "Paid" status.
Result: Customer charged, Odoo says "Draft". Double charge likely.
Robust Implementation
def action_capture_payment(self):
self.ensure_one()
# 1. Validation (Pre-Check)
if not self.payment_token_id:
raise UserError("No payment method selected.")
# 2. External API Call (Outside of critical DB locks if possible)
try:
response = payment_gateway.charge(self.amount_total, self.payment_token_id)
except GatewayTimeout:
_logger.error("Gateway Timeout for Order %s", self.name)
raise UserError("Payment gateway did not respond. Please try again.")
except Exception as e:
_logger.error("Payment Error %s: %s", self.name, str(e), exc_info=True)
raise UserError("An error occurred during payment processing.")
# 3. Process Response
if response['status'] == 'success':
# API succeeded, now update Odoo
try:
self.write({
'state': 'sale',
'transaction_id': response['id'],
'payment_date': fields.Datetime.now()
})
_logger.info("Payment captured for Order %s, Trans ID: %s", self.name, response['id'])
except Exception as e:
# CRITICAL: Money taken, DB failed. LOG LOUDLY.
_logger.critical(
"MONEY TAKEN BUT ODOO FAILED. Order: %s, Trans ID: %s. Error: %s",
self.name, response['id'], str(e)
)
else:
# API declined
_logger.info("Payment declined for Order %s: %s", self.name, response['message'])
raise UserError(f"Payment Declined: {response['message']}")
Monitoring & Alerting (Beyond Logs)
Logs are passive. You don't read them until something breaks. For critical errors, you need active notification.
Technique: The "System Issue" Model
Instead of just logging, create a record in a custom model or use Odoo's Activity system.
except Exception as e:
_logger.error("Sync Failed %s", order.name, exc_info=True)
# Create a To-Do activity for the IT Manager
order.activity_schedule(
'mail.mail_activity_data_warning',
user_id=self.env.ref('base.user_admin').id,
summary=f'Sync Failed: {str(e)}',
note='Check system logs for traceback.'
)
Technique: Scheduled Health Checks
Create a cron job that checks for "stuck" records.
Example: "Find all orders created > 24 hours ago that are confirmed but not synced."
Exception Types Comparison
| Exception Type | Logs Traceback? | Shows to User? | Use Case |
|---|---|---|---|
| UserError | No | Yes (popup) | Invalid user action (no shipping address) |
| ValidationError | No | Yes (popup) | Data integrity (negative price) |
| AccessError | Yes | Yes (access denied) | Security violation |
| Generic Exception | Yes | Yes (error page) | Unexpected system error |
Action Items: Audit Your Code
Audit Your Code
❏ grep your codebase for print(. Replace all with _logger
❏ grep for except: pass or naked except:. Delete immediately and handle specific exceptions
❏ Check all loops processing batch data (cron jobs). Do they use savepoint()?
❏ Check external API integrations. Are timeouts handled?
Improve Visibility
❏ Configure the Odoo log file path in odoo.conf so you can actually find it
❏ Add exc_info=True to all _logger.error calls
❏ Implement UserError for user-facing logic blocks instead of generic crashes
Frequently Asked Questions
When should I use UserError vs ValidationError in Odoo?
Use UserError for invalid user actions (ship order with no address, cancel confirmed sale). Use ValidationError for data integrity checks in @api.constrains (negative price, invalid email format). Both stop execution and show user-friendly popups.
Why should I use savepoint() in batch processing?
savepoint() isolates transactions per loop iteration. If processing 1,000 orders and record #999 fails, only that order rolls back—not all 999 previous orders. Critical for cron jobs and bulk operations to prevent cascading failures.
What's the difference between logging levels in Python?
DEBUG: Dev only, variable values. INFO: Successful actions. WARNING: Unexpected but handled. ERROR: Something broke. CRITICAL: Money lost, data corrupted. Always use exc_info=True with ERROR/CRITICAL to include stack traces.
How do I handle payment gateway failures safely?
Validate first (pre-check). Call API outside DB locks. If API succeeds but Odoo fails to save, log at CRITICAL level with transaction ID. Never silently swallow payment errors—use UserError to inform user and prevent double charges.
Free Code Quality Audit
Don't wait for a silent failure to cost you thousands. We'll scan your custom modules for dangerous try...except blocks, verify your transaction management in cron jobs, setup proper logging configuration, and stress-test your external integrations. Most D2C brands have critical vulnerabilities in their error handling logic. We find them before they break production.
