Enable Dark Mode!
how-to-handle-api-errors-effectively-in-odoo-19.jpg
By: Sayed Mahir Abdulla KK

How to Handle API Errors Effectively in Odoo 19

Technical Odoo 19 Odoo Enterprises Odoo Community

If you've built integrations against Odoo's JSON-RPC or REST endpoints, you've probably had this happen: you fire a request, get back a 200 OK, and then spend 20 minutes figuring out why your data was never saved. That's the thing about Odoo's API: errors don't always look like errors. A 200 response can still carry a JSON payload with "error" buried inside it.

Odoo 19 doesn't reinvent this, but it does tighten up how the ORM propagates errors through the API layer, and it introduces cleaner exception handling hooks in the web and base_rest modules. This post covers the practical side: what errors look like, how to catch them properly, and a few patterns that'll save you debugging time.

What Odoo 19 API Errors Actually Look Like

Odoo uses JSON-RPC 2.0 for most of its API surface. Here's what a failed call returns:

json{
 "jsonrpc": "2.0",
 "id": 1,
 "error": {
   "code": 200,
   "message": "Odoo Server Error",
   "data": {
     "name": "odoo.exceptions.ValidationError",
     "debug": "Traceback (most recent call last):\n ...",
     "message": "The field 'email' is required.",
     "arguments": ["The field 'email' is required."]
   }
 }
}

The outer code: 200 is misleading  it's part of the JSON-RPC spec and refers to an application-level error, not an HTTP status.

Common exception types in Odoo 19

Odoo ClassWhen it fires
ValidationErrorConstraint failures, required fields
AccessErrorRecord-level or model-level ACL
UserErrorBusiness logic violations
MissingErrorRecord deleted mid-transaction

Error Handling Patterns

1. Basic Python-side Handling in Custom Controllers

from odoo import http
from odoo.exceptions import ValidationError, AccessError, UserError
from odoo.http import request
import json
import logging
_logger = logging.getLogger(__name__)
class MyApiController(http.Controller):
   @http.route('/api/v1/partner/create', type=jsonrpc, auth='user', methods=['POST'])
   def create_partner(self, **kwargs):
       try:
           partner = request.env['res.partner'].create({
               'name': kwargs.get('name'),
               'email': kwargs.get('email'),
           })
           return {'success': True, 'partner_id': partner.id}
       except ValidationError as e:
           _logger.warning("Validation failed: %s", str(e))
           return {
               'success': False,
               'error_type': 'validation_error',
               'message': str(e),
           }
       except AccessError as e:
           _logger.error("Access denied for user %s: %s", request.env.user.name, str(e))
           return {
               'success': False,
               'error_type': 'access_error',
               'message': 'You do not have permission to perform this action.',
           }
       except UserError as e:
           return {
               'success': False,
               'error_type': 'user_error',
               'message': str(e),
           }
       except Exception as e:
           _logger.exception("Unexpected error in create_partner")
           return {
               'success': False,
               'error_type': 'server_error',
               'message': 'An internal error occurred. Please contact support.',
           }

One thing worth noting: never return raw Python tracebacks to external callers. Log them server-side, return a generic message to the client.

2. Handling Errors on the Client Side (JavaScript)

If you're calling Odoo's JSON-RPC from a frontend or external service, you need to check result.error even on a 200 response:

async function callOdooApi(endpoint, params) {
   const response = await fetch(endpoint, {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({
           jsonrpc: '2.0',
           method: 'call',
           id: Date.now(),
           params: params,
       }),
   });
   const data = await response.json();
   // JSON-RPC errors land here, not in HTTP status codes
   if (data.error) {
       const errorName = data.error?.data?.name;
       const errMsg = data.error?.data?.message || 'Unknown error';
if (errorName.includes("ValidationError")) {
  throw new Error(`Validation failed: ${errMsg}`);
} else if (errorName.includes("AccessError")) {
  throw new Error(`Permission denied: ${errMsg}`);
} else if (errorName.includes("UserError")) {
  throw new Error(`Business logic error: ${errMsg}`);

   }
   return data.result;
}
// Usage
try {
   const result = await callOdooApi('/web/dataset/call_kw', {
       model: 'res.partner',
       method: 'create',
       args: [{ name: 'Test', email: '' }],
       kwargs: {},
   });
   console.log('Created partner:', result);
} catch (err) {
   console.error('API call failed:', err.message);
}

3. Raising Custom Errors in Odoo 19 Models

from odoo import models, fields, api
from odoo.exceptions import ValidationError, UserError
class SaleOrderCustom(models.Model):
   _inherit = 'sale.order'
   @api.constrains('amount_total')
   def _check_minimum_order_value(self):
       for order in self:
           if order.amount_total < 50.0:
               raise ValidationError(
                   f"Order {order.name} must be at least $50.00. "
                   f"Current total: ${order.amount_total:.2f}"
               )
   def action_confirm(self):
       for order in self:
           if not order.partner_id.email:
               raise UserError(
                   "Cannot confirm order: customer has no email address on file."
               )
       return super().action_confirm()

The difference between ValidationError and UserError matters. Use ValidationError for data constraint violations (constraint decorators, @api.constrains). Use UserError for business rule violations that happen at runtime when the data is valid but the operation shouldn't proceed.

4. Using HTTP-style Responses in REST Controllers

If you're using base_rest or building REST endpoints directly, you get proper HTTP status codes:

from odoo import http
from odoo.http import request, Response
from odoo.exceptions import ValidationError, AccessError
import json
class RestApiController(http.Controller):
   @http.route('/api/v2/partner/<int:partner_id>', type='http', auth='user', methods=['GET'])
   def get_partner(self, partner_id, **kwargs):
       try:
           partner = request.env['res.partner'].browse(partner_id)
           if not partner.exists():
               return Response(
                   json.dumps({'error': 'Partner not found', 'code': 404}),
                   status=404,
                   content_type='application/json',
               )
           return Response(
               json.dumps({
                   'id': partner.id,
                   'name': partner.name,
                   'email': partner.email,
               }),
               status=200,
               content_type='application/json',
           )
       except AccessError:
           return Response(
               json.dumps({'error': 'Access denied', 'code': 403}),
               status=403,
               content_type='application/json',
           )
       except Exception as e:
           return Response(
               json.dumps({'error': 'Internal server error', 'code': 500}),
               status=500,
               content_type='application/json',
           )

5. Retrying on Transient Errors

Odoo uses PostgreSQL transactions, and under concurrent load you'll occasionally get psycopg2.errors. SerializationFailure usually when two requests try to write the same record at the same time. These are safe to retry:

import time
import psycopg2
from odoo import api, registry
def call_with_retry(db_name, model_name, method, args, max_retries=3, delay=0.5):
   for attempt in range(max_retries):
       try:
           with registry(db_name).cursor() as cr:
               env = api.Environment(cr, SUPERUSER_ID, {})
               result = getattr(env[model_name], method)(*args)
               cr.commit()
               return result
       except psycopg2.errors.SerializationFailure:
           if attempt < max_retries - 1:
               time.sleep(delay * (attempt + 1))
               continue
           raise

Don't retry ValidationError or UserError those won't resolve on their own.

To read more about How to Configure Odoo REST API Module in Odoo 18, refer to our blog How to Configure Odoo REST API Module in Odoo 18.


Frequently Asked Questions

Why am I getting 200 OK but the operation still failed?

Odoo's JSON-RPC always returns HTTP 200 unless there's a transport-level failure. Application errors live inside the response JSON, in data.error. Always check response.error before treating a call as successful. HTTP status codes only tell you whether the request arrived not whether it did anything useful.

What's the difference between UserError and ValidationError in Odoo 19?

ValidationError is for data integrity it fires when a value violates a constraint (wrong format, required field missing, domain check failed). UserError is for operational failures "you can't confirm this order because X." Both show up in the API response as user-facing messages, but raising the wrong one can confuse callers.

How do I catch an AccessError from an external API call?

Check error.data.includes("ValidationError") in the JSON-RPC response. On the server side, this usually means the authenticated user doesn't have read/write access to the model or record. Double-check your security groups (res.groups), record rules (ir.rule), and model access (ir.model.access). The debug traceback in error.data.debug will tell you exactly which rule blocked it.

Can I return custom error codes from an Odoo 19 controller?

Yes. If you're using type='json' routes, return a dict with whatever structure you want the error handling is yours to define. If you're using type='http' routes, return a Response object with the correct status code. What you can't do is change the outer JSON-RPC envelope — that's controlled by Odoo's dispatcher.

How do I log API errors without exposing stack traces to clients?

Use Python's logging module to write tracebacks server-side (_logger.exception(...) captures the full trace automatically), and return a sanitized message to the caller. Never serialize traceback.format_exc() directly into a response that goes outside your network. In production, set --log-level=warn and use a log aggregator to capture error-level entries from odoo.addons.

If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message