Errores GraphQL estructurados con codigos de extension
Implementa manejo de errores estructurado en GraphQL con clases de error personalizadas, codigos de extension y formato consistente para clientes
Los errores de GraphQL son solo objetos con un campo message. Sin estructura, los clientes recurren a comparar strings para determinar que fallo. Agregar extensions.code da a los clientes una categoria de error legible por maquina, mientras que campos adicionales de extension transportan contexto como detalles de validacion, hints de reintento o campos afectados. A continuacion se muestra como como construir un sistema de errores consistente en tu API GraphQL.
Cuando Usar Esto
-
For alternatives, see Detect and Fix N+1 Queries in GraphQL Resolvers.
-
Cualquier API GraphQL consumida por multiples clientes (web, movil, terceros)
-
APIs donde los clientes necesitan manejar diferentes tipos de error programaticamente
-
Servicios que necesitan distinguir entre validacion, auth, no encontrado y errores de servidor
Requisitos Previos
- Un servidor GraphQL (Apollo Server, GraphQL Yoga o Mercurius)
- Conocimiento basico de respuestas de error GraphQL
Solucion
1. Definir codigos de error
// errors/codes.ts
export const ErrorCodes = {
VALIDATION_ERROR: 'VALIDATION_ERROR',
UNAUTHENTICATED: 'UNAUTHENTICATED',
FORBIDDEN: 'FORBIDDEN',
NOT_FOUND: 'NOT_FOUND',
CONFLICT: 'CONFLICT',
RATE_LIMITED: 'RATE_LIMITED',
INTERNAL_ERROR: 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
} as const;
export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];
2. Crear un error base de GraphQL
// errors/base.ts
import { GraphQLError } from 'graphql';
import { ErrorCodes, type ErrorCode } from './codes';
export class GraphQLErrorExtension extends GraphQLError {
constructor(
message: string,
code: ErrorCode,
extensions?: Record<string, unknown>
) {
super(message, {
extensions: {
code,
...extensions,
},
});
this.name = this.constructor.name;
}
}
export class ValidationError extends GraphQLErrorExtension {
constructor(errors: { field: string; message: string }[]) {
super('Input validation failed', ErrorCodes.VALIDATION_ERROR, { errors });
}
}
export class AuthenticationError extends GraphQLErrorExtension {
constructor(message = 'Authentication required') {
super(message, ErrorCodes.UNAUTHENTICATED);
}
}
export class ForbiddenError extends GraphQLErrorExtension {
constructor(message = 'Insufficient permissions') {
super(message, ErrorCodes.FORBIDDEN);
}
}
export class NotFoundError extends GraphQLErrorExtension {
constructor(resource: string, id: string) {
super(`${resource} not found`, ErrorCodes.NOT_FOUND, { resource, id });
}
}
export class ConflictError extends GraphQLErrorExtension {
constructor(message: string, conflicts?: Record<string, unknown>) {
super(message, ErrorCodes.CONFLICT, { conflicts });
}
}
export class RateLimitError extends GraphQLErrorExtension {
constructor(retryAfter: number) {
super('Rate limit exceeded', ErrorCodes.RATE_LIMITED, { retryAfter });
}
}
3. Usar errores en los resolvers
// resolvers.ts
import { ValidationError, NotFoundError, AuthenticationError, ForbiddenError } from './errors/base';
export const resolvers = {
Query: {
post: async (_: unknown, { id }: { id: string }, ctx: Context) => {
if (!ctx.user) throw new AuthenticationError();
const post = await ctx.db.posts.findById(id);
if (!post) throw new NotFoundError('Post', id);
if (post.authorId !== ctx.user.id && ctx.user.role !== 'admin') {
throw new ForbiddenError('You can only view your own posts');
}
return post;
},
},
Mutation: {
createPost: async (_: unknown, { input }: { input: any }, ctx: Context) => {
if (!ctx.user) throw new AuthenticationError();
const errors = validatePostInput(input);
if (errors.length > 0) throw new ValidationError(errors);
const existing = await ctx.db.posts.findBySlug(input.slug);
if (existing) throw new ConflictError('Slug already exists', { slug: input.slug });
return ctx.db.posts.create({ ...input, authorId: ctx.user.id });
},
},
};
function validatePostInput(input: any): { field: string; message: string }[] {
const errors: { field: string; message: string }[] = [];
if (!input.title || input.title.length < 3) {
errors.push({ field: 'title', message: 'Title must be at least 3 characters' });
}
if (!input.content || input.content.length < 10) {
errors.push({ field: 'content', message: 'Content must be at least 10 characters' });
}
return errors;
}
4. Configurar formato de errores en Apollo Server
// server.ts
import { ApolloServer } from '@apollo/server';
import { GraphQLError } from 'graphql';
import { ErrorCodes } from './errors/codes';
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (formatted, error) => {
if (error?.extensions?.code) {
return {
message: formatted.message,
extensions: {
code: error.extensions.code,
...error.extensions,
},
};
}
return {
message: 'Internal server error',
extensions: {
code: ErrorCodes.INTERNAL_ERROR,
},
};
},
});
5. Manejo de errores en el cliente
// client.ts
async function createPost(input: CreatePostInput) {
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: CREATE_POST_MUTATION, variables: { input } }),
});
const { data, errors } = await response.json();
if (errors) {
for (const error of errors) {
switch (error.extensions.code) {
case 'VALIDATION_ERROR':
console.error('Validation:', error.extensions.errors);
break;
case 'UNAUTHENTICATED':
window.location.href = '/login';
break;
case 'NOT_FOUND':
console.error('Not found:', error.extensions.resource);
break;
case 'RATE_LIMITED':
const retryAfter = error.extensions.retryAfter;
console.log(`Rate limited. Retry in ${retryAfter}s`);
break;
default:
console.error('Unexpected:', error.message);
}
}
return null;
}
return data.createPost;
}
Como Funciona
GraphQLErrorExtensionenvuelveGraphQLErrorcon uncodeobligatorio en extensions, asegurando que cada error tenga una categoria legible por maquina- Clases especializadas (
ValidationError,NotFoundError, etc.) transportan contexto especifico del dominio en extensions mientras comparten una estructura consistente formatErroren Apollo Server actua como red de seguridad — asegura que los errores internos no filtren stack traces y que todos los errores tengan un codigo- Los clientes hacen switch en
extensions.codeen lugar de parsear strings de mensaje, haciendo el manejo de errores robusto ante cambios de mensaje
Variantes
Plugin de logging de errores
Registra errores con metadata estructurada sin filtrar a clientes:
const errorLogger = {
async requestDidStart() {
return {
async didEncounterErrors(requestContext) {
for (const error of requestContext.errors) {
if (error.extensions?.code === ErrorCodes.INTERNAL_ERROR) {
logger.error({
query: requestContext.operationName,
variables: requestContext.request.variables,
error: error.message,
stack: error.stack,
});
}
}
},
};
},
};
Union types para errores esperados
Para errores que son parte del dominio (no excepcionales), modelalos como union types:
union CreatePostResult = Post | ValidationError | AuthError
type Mutation {
createPost(input: CreatePostInput!): CreatePostResult!
}
Esto da a los clientes manejo de errores type-safe sin try/catch.
Mejores Practicas
- Siempre incluye
extensions.code— es el contrato entre servidor y cliente para manejo de errores - Nunca filtres stack traces en produccion —
formatErrordebe eliminarlos - Registra errores internos en el servidor — los clientes ven un mensaje generico, tus logs ven el trace completo
- Mantén los mensajes accionables — dile al cliente que corregir, no solo que se rompio
Errores Comunes
- Usar codigos de estado HTTP para errores GraphQL — GraphQL siempre retorna 200; los errores van en el body
- Lanzar
Errorplano — le faltaextensions.code, forzando a los clientes a comparar strings - Exponer mensajes internos — errores de base de datos, rutas de archivo o stack traces filtran detalles de implementacion
- Sobrecargar
INTERNAL_ERROR— si un error es esperado (no encontrado, validacion), dale su propio codigo
Errores Comunes Adicionales
- Retornar HTTP 200 con solo
errorsy sindata— los clientes esperan quedataesté presente aunque sea null - Exponer stack traces en producción — siempre elimina
extensions.exception.stacktrace - Usar campos non-null donde son posibles los errores — causa nulls en cascada que borran datos parciales útiles
- No loguear errores en el servidor — el cliente ve el error, pero el servidor debería loguear el contexto completo para debugging
- Usar
INTERNAL_SERVER_ERRORgenérico para todos los fallos — los clientes no pueden diferenciar entre problemas de red, errores de validación y bugs del servidor - No envolver funciones resolver en try-catch — las excepciones no manejadas crashean todo el pipeline de requests
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Preguntas frecuentes
Debo usar errores GraphQL o union types para fallos esperados?
Usa errores para casos excepcionales (servidor caido, fallo de auth). Usa union types para resultados del dominio que los clientes necesitan manejar en la UI (validacion, no encontrado, conflicto).
Como pruebo respuestas de error?
Verifica errors[0].extensions.code en tu cliente de pruebas. Es mas estable que verificar el texto del mensaje.
Puedo agregar campos de extension personalizados?
Si. Extensions es un mapa abierto. Agrega retryAfter, field, conflicts o cualquier metadata que los clientes necesiten.
Debo localizar mensajes de error?
Retorna codigos de error y nombres de campo en extensions. Deja que los clientes localicen el mensaje basandose en el codigo y el locale del usuario.
Recursos Relacionados
Validacion y sanitizacion de input types en GraphQL en
Implementa validacion centralizada de inputs en GraphQL con funciones personalizadas, schemas Zod y transformaciones de input types
RecipeConstruye una API GraphQL con Apollo Server y TypeScript
Como construir una API GraphQL lista para produccion usando Apollo Server, TypeScript y DataLoader para resolver el problema de consultas N+1
RecipeManejar Errores en APIs con RFC 7807
Patrones para un manejo de errores de API consistente y predecible en varios lenguajes y frameworks.
RecipeAuth a nivel campo con directivas GraphQL personalizadas
Agrega autorización a nivel campo en GraphQL con directivas de schema personalizadas. Verifica roles, permisos y propiedad por campo en Apollo Server.
RecipeMocks de resolvers GraphQL para desarrollo frontend
Configura resolvers GraphQL mockeados con Apollo Server para que los equipos frontend desarrollen contra una API falsa antes del backend.