The exceptions you throw from your service layer are should ideally be related to the business logic of your domain. For example you will maybe throw an IllegalArgumentException
if someone wanted to update a User
object that did not exist. In general you only want to throw custom exceptions "if they do have useful information for client code" (see this answer).
If there are complications on the Controller layer, e.g., an invalid value provided by the client for a custom header, then you are perfectly fine throwing an exception in the controller layer, too.
As you have already mentioned, in general you then want to have one or more global exception handlers, which can convert the thrown Exceptions into a more useful http response for the client, probably in json format and with a proper HTTP Status Code (e.g., internal server error) and maybe a descriptive message. But you can also be more specific than that and return different http status code and payloads in different scenarios. For example, maybe in the case where someone wanted to update a user that did not exist you want to return a 404 status code. Check out the more flexible ResponseEntity
return type for that.
Regarding error codes, you have to ask yourself if the client is benefiting from that or if they are more happy with a short message or do not care at all.