Manejo de Errores10 temas

Manejo de Errores

Un manejo de errores consistente es lo que separa una API amateur de una profesional. Cuando todos los errores tienen el mismo formato, los clientes (frontend, mobile, otros servicios) pueden parsearlos y reaccionar sin adivinar.

1. ValidationError en Serializers

El primer filtro de datos inválidos. DRF lo maneja automáticamente en los serializers:

from rest_framework import serializers


class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = '__all__'

    # Validación por campo
    def validate_price(self, value):
        if value <= 0:
            raise serializers.ValidationError("El precio debe ser mayor a 0.")
        return value

    # Validación cross-field
    def validate(self, attrs):
        if attrs.get('discount') and attrs.get('discount') > attrs.get('price', 0):
            raise serializers.ValidationError(
                "El descuento no puede ser mayor al precio."
            )
        return attrs

2. Excepciones estándar de DRF

Excepción Código Cuándo usarla
NotFound 404 Recurso no existe
PermissionDenied 403 Sin permisos
AuthenticationFailed 401 Credenciales inválidas
MethodNotAllowed 405 Método HTTP no soportado
Throttled 429 Límite de tasa excedido
ValidationError 400 Datos inválidos (en serializers)
from rest_framework.exceptions import NotFound, PermissionDenied

class ProductDetailView(APIView):
    def get(self, request, pk):
        try:
            product = Product.objects.get(pk=pk)
        except Product.DoesNotExist:
            raise NotFound("Producto no encontrado.")

        if not request.user.has_perm('view_product', product):
            raise PermissionDenied("No tenés permiso para ver este producto.")

        return Response(ProductSerializer(product).data)

3. Excepciones de negocio personalizadas

Cuando las reglas del dominio requieren errores específicos:

from rest_framework.exceptions import APIException


class ResourceLockedException(APIException):
    status_code = 423
    default_detail = "El recurso está bloqueado."
    default_code = "resource_locked"


class InsufficientStockException(APIException):
    status_code = 400
    default_detail = "Stock insuficiente para completar la operación."
    default_code = "insufficient_stock"


class DuplicateResourceException(APIException):
    status_code = 409
    default_detail = "El recurso ya existe."
    default_code = "duplicate"

Con mensaje dinámico:

class BusinessRuleException(APIException):
    status_code = 422
    default_code = "business_rule_violation"

    def __init__(self, message, code=None):
        self.detail = message
        if code:
            self.default_code = code

4. Custom Exception Handler global

Estandariza el formato de TODAS las respuestas de error en un solo lugar:

# core/handlers.py
from rest_framework.views import exception_handler


def custom_exception_handler(exc, context):
    """
    Normaliza todas las respuestas de error al mismo formato.
    """
    response = exception_handler(exc, context)

    if response is not None:
        response.data = {
            'success': False,
            'error': {
                'code': exc.__class__.__name__,
                'message': _extract_message(exc),
                'status_code': response.status_code,
            }
        }

    return response


def _extract_message(exc):
    """Extrae el mensaje de forma segura."""
    if hasattr(exc, 'detail'):
        if isinstance(exc.detail, dict):
            return exc.detail  # Errores por campo
        return str(exc.detail)
    return str(exc)

Registralo en settings:

# settings.py
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'core.handlers.custom_exception_handler',
}

Sin handler:

{"detail": "No encontrado."}

Con handler:

{
    "success": false,
    "error": {
        "code": "NotFound",
        "message": "No encontrado.",
        "status_code": 404
    }
}

Cuándo usar cada mecanismo

Mecanismo Para qué Status
ValidationError en serializer Datos inválidos del request 400
Excepciones built-in Casos estándar (404, 403, 401) Varios
Excepciones de negocio Reglas de dominio violadas 400-422
Custom handler global Estandarizar formato de TODOS los errores

Errores comunes

  • No registrar el handler: Sin la config en settings.py, no se usa.
  • Excepciones redundantes: No crear un NotFound propio si DRF ya lo tiene.
  • Mezclar validación con reglas de negocio: ValidationError es para datos mal formados; las reglas de dominio van en excepciones custom.
  • Olvidar if response is not None: Si la excepción no es de DRF, el handler debe retornar None para que Django la maneje como 500.

Recursos

DRF Exceptions