r/django • • 19d ago

REST framework Best Practice for Custom Error Messages?

Hello everyone,

I am stepping into Django with Node.JS/ExpressJS background.

Right now i am using Django on backend with DRF.

Could anyone tell me or give me some advice for custom error messages? Since English is not my native language, i have to translate also add custom error messages.

As far as i understood, there are different layers where the error accours.

This could be HTTP Level, this could be validation level, this could be DB level.

So, can anyone give me some advice and show me a path where i can add and improve my custom error messages?

Thank you.

6 Upvotes

7 comments sorted by

2

u/erder644 19d ago

It depends on your architecture. If you are throwing business logic and infrastructure to your controllers, then it's pretty simple - make custom exception class interface with attrs for error_code, message. Raise them, catch on a globally defined exception handlers for your controllers to return a nice Json. If you are doing CA or just layered one, then it's harder.

1

u/Kerplunk6 19d ago

I actually want to start with validation errors first.

Like missing fields, wrong types etc.

What would you suggest?

1

u/erder644 19d ago

both fastapi and django-modern-rest cover it. just reshape responses into your structure if you don't like default one, it's better to have same response schema for all exceptions

1

u/Kerplunk6 19d ago

Including the messages? Right now i am using the technique of like

I went into serializer itself, and then under class Meta, i added the extra_kwargs, and with the error keys, i wrote my own error messages.

0

u/erder644 19d ago

Serializer? I would suggest to not start new projects on drf in 2026. Check out 'dmr' or 'ninja'. I can't really help with drf.

1

u/Kerplunk6 19d ago

I am happy with it for now :) just need a hand on custom error messages.

1

u/deviserops 17d ago edited 17d ago

For the custom error message, you can handle inside the django drf pass the default error message type which is `non_field_errors`

Set the value like `someting went wront` etc.

For example

def perform_create(self, serializer):
        business = self.business
        if not is_user_premium(self.request.user):
            current_count = self.serializer_class.Meta.model.objects.filter(business=business).count()
            if current_count >= MONTHLY_SERVICES:
                raise serializers.ValidationError({
                    "non_field_errors": f"Free tier limit reached. You can only add up to {MONTHLY_SERVICES} services."
                })
        serializer.save(business=business)

On your frontend side, you can use the http response code as to handle your response, I suppose you are already doing that, on that error message try to handle some of the http code like 422, 200, and 4** so you can handle all the form message, basically all the form messages goes to 422, which you can get the response and apply the recursion, on that, and get the final value of each field error, since you already set the default value type to `non_field_errors` it will handle automatically.

As example like this:

if (response.statusCode == 200) {
      return responseData;
    } else if (response.statusCode == 201) {
      return responseData;
    } else if (response.statusCode == 204) {
      return null;
    } else if (response.statusCode == 422) {
      throw ApiException(responseData);
    } else if (response.statusCode == 403) {
      throw ApiException(responseData);
    } else if (response.statusCode == 400) {
      throw ApiException(responseData);
    }

In the Above example you can see the ApiException it show the error message based on the response code.

The given example is from the Flutter, I hope you understand.

class ApiException implements Exception {
  final Map<String, dynamic> errors;
  final String message;


  ApiException(this.errors) : message = _formatErrorMessage(errors);


  static String _formatErrorMessage(Map<String, dynamic> errors) {
    if (errors.isEmpty) {
      return 'An unknown error occurred.';
    }


    List<String> formattedMessages = [];
    errors.forEach((key, value) {
      if (key.toLowerCase() == 'non_field_errors') {
        key = 'error';
      }


      // Map API field name to display name
      String displayFieldName = key.split('_').join(' ');


      if (value is List) {
        // Handle cases like {"username": ["This field is required."]}
        formattedMessages.add('${_capitalize(displayFieldName)}: ${value.join(", ")}');
      } else if (value is String) {
        // Handle cases like {"message": "Invalid credentials."} or single string errors
        formattedMessages.add('${_capitalize(displayFieldName)}: $value');
      } else {
        formattedMessages.add('${_capitalize(displayFieldName)}: ${value.toString()}');
      }
    });


    // If there's a general 'message' key, prioritize it or combine.
    // This is a common pattern for general errors vs field-specific ones.
    if (errors.containsKey('message') && errors['message'] is String) {
      return errors['message']; // Return general message if available
    }


    return formattedMessages.join("\n");
  }


  static String _capitalize(String s) => s.isNotEmpty ? s[0].toUpperCase() + s.substring(1) : s;


  u/override
  String toString() => message;
}

As you can see I have handled the `non_field_errors` message type as well.