Skip to main content

Overview

Satélite API implements structured error handling through GraphQL exceptions and a custom error filter. This ensures consistent error responses across the API and provides meaningful error information to clients.

GraphQL Exception Pattern

The API uses GraphQLException to throw errors that are properly formatted in GraphQL responses. This pattern is used consistently throughout the codebase.

Basic Error Throwing

Schema/Query/Query.cs:473
public async Task<UsuarioDTO> GetUsuarioByEmail(string mail)
{
    try
    {
        UsuarioDTO usuario = await _userRepository.GetUserByEmail(mail);
        if (usuario is null)
        {
            throw new GraphQLException(
                new Error("UsuarioDTO NOT_FOUND.", "USUARIODTO_NOT_FOUND"));
        }
        return usuario;
    }
    catch (Exception e)
    {
        throw new GraphQLException(new Error("FAIL.", "FAIL"));
    }
}

Error Constructor Pattern

throw new GraphQLException(new Error(message, errorCode));
Parameters:
  • message: Human-readable error description
  • errorCode: Machine-readable error code for client handling
The error code should be in SCREAMING_SNAKE_CASE format (e.g., USUARIODTO_NOT_FOUND, PRODUCTODTO_NOT_FOUND).

Common Error Patterns

1. Not Found Errors

public async Task<UsuarioDTO> GetUsuarioById(int id)
{
    try
    {
        UsuarioDTO usuario = await _userRepository.GetById(id);
        if (usuario is null)
        {
            throw new GraphQLException(
                new Error("UsuarioDTO NOT_FOUND.", "USUARIODTO_NOT_FOUND"));
        }
        return usuario;
    }
    catch (Exception e)
    {
        throw new GraphQLException(new Error("FAIL.", "FAIL"));
    }
}

2. Validation Errors in Mutations

Schema/Mutation/Mutation.cs:437
public async Task<UsuarioDTO> UpdateUser(UsuarioDTO user)
{
    UsuarioDTO usuario = await _usuarioRepository.GetById(user.Id);
    if (usuario is null)
    {
        throw new GraphQLException(
            new Error("UsuarioDTO not found.", "UsuarioDTO_NOT_FOUND"));
    }

    usuario.Nombre = user.Nombre;
    usuario.Email = user.Email;
    usuario.UpdatedAt = DateTime.UtcNow.AddHours(-5);
    usuario = await _usuarioRepository.Update(usuario);

    return usuario;
}

3. Business Logic Errors

Schema/Mutation/Mutation.cs:666
public async Task<ProductoDTO> CreateProducto(ProductoDTO product)
{
    CenizaDTO ceniza = await _cenizaRepository.GetById(product.IdCeniza);
    if (ceniza == null)
    {
        throw new GraphQLException(
            new Error("Ceniza not found.", "CENIZA_NOT_FOUND"));
    }

    ProductoDTO producto = new ProductoDTO()
    {
        Tipo = product.Tipo,
        Pasaje = product.Pasaje,
        Cantidad = product.Cantidad,
        IdCeniza = ceniza.Id,
        Activo = true,
        CreatedAt = DateTime.UtcNow.AddHours(-5),
    };
    producto = await _productoRepository.Create(producto);
    return producto;
}

4. Generic Exception Handling

Schema/Query/Query.cs:974
public async Task<IEnumerable<IndAntAprobacionDTO>> GetAllAprobacionesTodas()
{
    try
    {
        return await _indAntAprobacionRepository.GetAll();
    }
    catch (Exception ex)
    {
        throw new GraphQLException(
            new Error("FAIL.", "FAIL " + ex.Message));
    }
}

GraphQL Error Filter

The API uses a custom error filter to process exceptions and format error responses. This is configured in Program.cs:737:
Program.cs:737
builder.Services.AddErrorFilter<GraphQLErrorFilter>();

Error Filter Implementation

Location: Extensions/GraphQLErrorFilter.cs
Extensions/GraphQLErrorFilter.cs:6-35
public sealed class GraphQLErrorFilter : IErrorFilter
{
    private readonly ILogger<AprobacionNotaCreditoService> _logger;

    public GraphQLErrorFilter(
        ILogger<AprobacionNotaCreditoService> logger) => _logger = logger;

    public IError OnError(IError error)
    {
        var ex = error.Exception;
        _logger.LogError("Exception: {ex}", error);
        
        if (ex is SateliteException de)
        {
            return ErrorBuilder
                .FromError(error)
                .SetMessage(de.Message)
                .SetCode(de.Code ?? "DOMAIN_ERROR")
                .SetExtension("status", de.Status)
                .Build();
        }
        
        return error;
    }
}

Filter Responsibilities

1

Error Logging

All errors are logged with full exception details for debugging:
_logger.LogError("Exception: {ex}", error);
2

Custom Exception Handling

Special handling for SateliteException domain exceptions:
if (ex is SateliteException de)
{
    return ErrorBuilder
        .FromError(error)
        .SetMessage(de.Message)
        .SetCode(de.Code ?? "DOMAIN_ERROR")
        .SetExtension("status", de.Status)
        .Build();
}
3

Default Error Passthrough

Other exceptions are passed through as-is:
return error;
The commented code shows an alternative implementation that could hide error details in production:
//return ErrorBuilder
//    .FromError(error)
//    .SetMessage(_env.IsDevelopment() 
//        ? (ex?.Message ?? error.Message) 
//        : "Se ha producido un error inesperado")
//    .SetCode("UNEXPECTED_ERROR")  
//    .Build();

Error Response Format

When an error occurs, clients receive a structured GraphQL error response:
GraphQL Error Response
{
  "errors": [
    {
      "message": "UsuarioDTO NOT_FOUND.",
      "extensions": {
        "code": "USUARIODTO_NOT_FOUND"
      },
      "path": ["getUsuarioById"],
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ]
    }
  ],
  "data": null
}

Error Response Fields

FieldDescription
messageHuman-readable error description
extensions.codeMachine-readable error code
pathGraphQL query path where error occurred
locationsLine and column in the GraphQL query

Custom Domain Exceptions

The API defines custom exceptions for domain-specific errors:
Example: SateliteException
public class SateliteException : Exception
{
    public string Code { get; set; }
    public int Status { get; set; }

    public SateliteException(string message, string code, int status = 400) 
        : base(message)
    {
        Code = code;
        Status = status;
    }
}
Usage:
if (invalidCondition)
{
    throw new SateliteException(
        "Invalid operation",
        "INVALID_OPERATION",
        400
    );
}

Error Handling Best Practices

Always Validate Input

Check for null or invalid data before processing:
if (entity is null)
{
    throw new GraphQLException(
        new Error("Entity not found", "NOT_FOUND"));
}

Use Descriptive Error Codes

Error codes should clearly identify the error:
"USUARIODTO_NOT_FOUND"
"PRODUCTODTO_NOT_FOUND"
"CENIZA_NOT_FOUND"

Provide Context

Include relevant information in error messages:
throw new GraphQLException(
    new Error($"Product {id} not found", 
             "PRODUCT_NOT_FOUND"));

Log Exceptions

The error filter automatically logs all exceptions for debugging and monitoring.

Common Error Codes

Here are the standard error codes used throughout the API:
Error CodeDescription
USUARIODTO_NOT_FOUNDUser not found
PRODUCTODTO_NOT_FOUNDProduct not found
CENIZADTO_NOT_FOUNDCeniza record not found
PRODUCTODETALLEDTO_NOT_FOUNDProduct detail not found
APISRIDTO_NOT_FOUNDSRI API record not found
RolesDTO_NOT_FOUNDRole not found
PermisoDTO_NOT_FOUNDPermission not found
RolPermisoDTO_NOT_FOUNDRole-permission mapping not found
IndSupPqrDTO_NOT_FOUNDPQR not found
IndSupSolpedDTO_NOT_FOUNDSolped not found
CENIZA_NOT_FOUNDCeniza not found (mutation variant)
DOMAIN_ERRORGeneric domain error
FAILGeneric failure
Consistency: Always use the same error code for the same type of error across the API. This makes it easier for clients to handle errors programmatically.

Try-Catch Patterns

Pattern 1: Specific Error with Fallback

public async Task<UsuarioDTO> GetUsuarioByEmail(string mail)
{
    try
    {
        UsuarioDTO usuario = await _userRepository.GetUserByEmail(mail);
        if (usuario is null)
        {
            throw new GraphQLException(
                new Error("UsuarioDTO NOT_FOUND.", "USUARIODTO_NOT_FOUND"));
        }
        return usuario;
    }
    catch (Exception e)
    {
        throw new GraphQLException(new Error("FAIL.", "FAIL"));
    }
}

Pattern 2: Direct Validation (No Try-Catch)

public async Task<ProductoDTO> GetProductoById(int id)
{
    ProductoDTO producto = await _productoRepository.GetById(id);
    if (producto is null)
    {
        throw new GraphQLException(
            new Error("ProductoDTO NOT_FOUND.", "PRODUCTODTO_NOT_FOUND"));
    }
    return producto;
}

Pattern 3: Exception Message Forwarding

public async Task<IEnumerable<IndAntAprobacionDTO>> GetAllAprobacionesTodas()
{
    try
    {
        return await _indAntAprobacionRepository.GetAll();
    }
    catch (Exception ex)
    {
        throw new GraphQLException(
            new Error("FAIL.", "FAIL " + ex.Message));
    }
}
Recommendation: Use Pattern 2 (direct validation) for simple null checks, and Pattern 1 (try-catch with fallback) when database operations might throw specific exceptions that need to be handled differently.

Testing Error Scenarios

When testing the API, you can verify error handling by:
Test Not Found Error
query {
  getUsuarioById(id: 99999) {
    id
    nombre
    email
  }
}
Expected Response:
{
  "errors": [
    {
      "message": "UsuarioDTO NOT_FOUND.",
      "extensions": {
        "code": "USUARIODTO_NOT_FOUND"
      }
    }
  ],
  "data": null
}

Next Steps

GraphQL Overview

Learn about GraphQL implementation

Architecture

Understand the layered architecture

Build docs developers (and LLMs) love