для ошибок уже есть RFC 7807, в c# уже есть готовый ProblemDetail
https://datatracker.ietf.org/doc/html/rfc7807
не выявленные ошибки также нужно отлавливать в middlware
вот пример
public class ResponseResult<T> : IResponseResult<T>
    {
        public bool IsSuccess { get; private set; }
        public ProblemDetails? Error { get; private set; }
        public T? Data { get; private set; }
        public static ResponseResult<T> Success(T data) => new ResponseResult<T> { IsSuccess = true, Data = data };
        public static ResponseResult<T> Success() => new ResponseResult<T> { IsSuccess = true};
        public static ResponseResult<T> Failure(Exception exception) => new ResponseResult<T>
        {
            IsSuccess = false,
            Error = new ProblemDetails
            {
                Title = GetTitleByException(exception),
                Detail = exception.Message,
                Status = exception switch
                {
                    ArgumentException => StatusCodes.Status400BadRequest,
                    KeyNotFoundException => StatusCodes.Status404NotFound,
                    UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
                    _ => StatusCodes.Status500InternalServerError
                },
                Instance = Guid.NewGuid().ToString()
            }
        };
        private static string GetTitleByException(Exception exception) =>
            exception switch
            {
                ArgumentException => "Invalid argument",
                KeyNotFoundException => "Resource not found",
                UnauthorizedAccessException => "Unauthorized",
                _ => "Internal server error"
            };
    }
Слой бизнес логики
return ResponseResult<bool>.Failure(
                    new KeyNotFoundException(
                        string.Format("User with login '{0}' not found", request.Login))
                    );
Презентационный слой 
[HttpGet("verify")]
        public async Task<ActionResult> VerifyUser(
            [FromQuery] string login,
            [FromQuery] string password,
            CancellationToken cancellationToken)
        {
            var result = await mediator.Send(new VerifyQuery() { Login = login, Password = password }, cancellationToken);
            if (result.IsSuccess)
            {
                return Ok(new { verify = result.Data });
            }
            return StatusCode(result.Error?.Status ?? 400, result.Error);
        }