Errors and Validation
GraphQL returns data and errors together; Nest maps HttpExceptions onto error codes, and ValidationPipe still guards your inputs.
throw new GraphQLError(message, { extensions: { code: 'MY_CODE' } })A GraphQL response is an object with data and errors, and both can be present at once. When a resolver throws and the field is nullable, that field becomes null, an entry appears in errors with a path pointing at it, and the rest of the response is still delivered. When the field is non-null the null has nowhere to go, so it propagates up to the nearest nullable parent — which can blank a whole branch for one failed leaf.
Status codes and error codes
Resolver failures do not set the HTTP status. The Apollo driver keeps execution errors at 200 so the body always has the { data, errors } shape; preserveHttpStatusForExecutionErrors: false lets a status from extensions.http.status through instead. What clients branch on is extensions.code. With autoTransformHttpErrors (default true) the driver rewrites Nest exceptions: 400 becomes BAD_REQUEST, 401 UNAUTHENTICATED, 403 FORBIDDEN, 422 BAD_USER_INPUT, and anything else INTERNAL_SERVER_ERROR with the status in extensions.status. The exception's response body is attached as extensions.originalError.
Validating input
A pipe is a class that transforms or rejects a handler argument before the handler runs. ValidationPipe works on resolvers exactly as on controllers: put class-validator rules on an @InputType() or @ArgsType() class, take it through @Args(), and register the pipe globally.
An exception filter catches what a handler throws. A GraphQL one returns an error instead of writing a response, because there is no response to write to.
Example
// src/users/users.resolver.ts
import { NotFoundException } from '@nestjs/common';
import { Args, ID, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UserInputError } from '@nestjs/apollo';
import { GraphQLError } from 'graphql';
import { User } from './user.model.js';
import { UsersService } from './users.service.js';
Resolver(() => User)
export class UsersResolver {
constructor(private readonly users: UsersService) {}
Query(() => User, { nullable: true })
async user(Args('id', { type: () => ID }) id: string) {
const user = await this.users.findOneById(Number(id));
if (!user) {
// A plain Nest exception. The Apollo driver turns it into a GraphQL
// error whose extensions.code is INTERNAL_SERVER_ERROR with
// extensions.status 404, because 404 is not in the mapped set.
// The field is nullable, so `data.user` is null and the rest of the
// response still arrives.
throw new NotFoundException(`User ${id} not found`);
}
return user;
}
Mutation(() => Boolean)
async transferCredits(
Args('from', { type: () => ID }) from: string,
Args('to', { type: () => ID }) to: string,
) {
if (from === to) {
// @nestjs/apollo re-exports the error classes Apollo Server 4 removed.
// This one sets extensions.code to BAD_USER_INPUT for you.
throw new UserInputError('from and to must differ', {
extensions: { argumentName: 'to' },
});
}
// Any code you invent: the client branches on it, so treat it as API
// surface and keep it stable.
throw new GraphQLError('Insufficient credits', {
extensions: { code: 'INSUFFICIENT_CREDITS', available: 12 },
});
}
}
When to use it
- Return the rest of a dashboard when one widget's upstream call fails.
- Give clients a stable extensions.code to branch on instead of parsing error messages.
- Validate a mutation's input object with class-validator before the resolver runs.
- Strip stack traces and ORM messages out of production error responses.
More examples
ValidationPipe over an @InputType()
This is the whole wiring: rules on the input class, the pipe registered once at bootstrap. The pipe runs after guards and interceptors and before the resolver method, so an invalid mutation never reaches your service. Note where the two validations divide — GraphQL checks that `title` is a String and present, class-validator checks that it is 3 to 120 characters.
// src/posts/dto/create-post.input.ts
import { Field, InputType, Int } from '@nestjs/graphql';
import { IsIn, IsNotEmpty, Length, Min } from 'class-validator';
@InputType()
export class CreatePostInput {
@Field()
@Length(3, 120)
title: string;
@Field()
@IsNotEmpty()
body: string;
@Field(() => Int, { defaultValue: 0 })
@Min(0)
votes: number;
@Field(() => String, { defaultValue: 'draft' })
@IsIn(['draft', 'published'])
status: string;
}
// src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
// Drops properties with no decorator. GraphQL already rejects unknown
// input fields at validation time, so this mostly matters for nested
// objects reached through JSON scalars.
whitelist: true,
forbidNonWhitelisted: true,
// Turns the plain object from the query into a CreatePostInput
// instance, which is what class-validator needs to read its metadata.
transform: true,
}),
);
await app.listen(3000);
}
bootstrap();
A GraphQL exception filter
Reach for a filter when you want one place that decides the public shape of an error class, or that logs which field failed. Remember the ordering: filters are last, and in @nestjs/graphql 14 they do not run on @ResolveField() methods unless 'filters' is listed in fieldResolverEnhancers.
// src/common/gql-http-exception.filter.ts
import { ArgumentsHost, Catch, HttpException, Logger } from '@nestjs/common';
import { GqlArgumentsHost, GqlExceptionFilter } from '@nestjs/graphql';
import { GraphQLError } from 'graphql';
// @Catch narrows which exceptions reach this filter.
@Catch(HttpException)
export class GqlHttpExceptionFilter implements GqlExceptionFilter {
private readonly logger = new Logger(GqlHttpExceptionFilter.name);
catch(exception: HttpException, host: ArgumentsHost) {
// ArgumentsHost is transport-agnostic. GqlArgumentsHost.create() reinterprets
// it as the four GraphQL resolver arguments: root, args, context, info.
const gqlHost = GqlArgumentsHost.create(host);
const info = gqlHost.getInfo<{
fieldName: string;
parentType: { name: string };
}>();
this.logger.warn(
`${info.parentType.name}.${info.fieldName} failed: ${exception.message}`,
);
// Return, do not write. A GraphQL filter has no response object; whatever
// it returns is placed in the errors array for this field's path.
return new GraphQLError(exception.message, {
extensions: {
code: exception.getStatus() === 404 ? 'NOT_FOUND' : 'BAD_REQUEST',
field: info.fieldName,
},
});
}
}
Hiding internals with formatError
Use this before an endpoint is public. extensions.originalError carries the HttpException response body, which for a validation failure includes the full class-validator message array — useful in development, an information leak in production. Keeping intentional codes and flattening unexpected ones is the usual split.
// src/app.module.ts (GraphQL section only)
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { ApolloServerErrorCode } from '@apollo/server/errors';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
// When on, Apollo puts the stack in extensions.stacktrace of every
// error it returns. Off in production, always.
includeStacktraceInErrorResponses: process.env.NODE_ENV !== 'production',
// Runs last, on every error, after the driver's own HttpException
// mapping. Returning a new object is how you drop fields.
formatError: (formattedError, error) => {
const code = formattedError.extensions?.code;
if (code === ApolloServerErrorCode.INTERNAL_SERVER_ERROR) {
// A driver message, a stack, extensions.originalError and the ORM's
// own text all live in the untouched error. Replace the lot.
return {
message: 'Internal server error',
path: formattedError.path,
locations: formattedError.locations,
extensions: { code },
};
}
// Deliberate errors (BAD_USER_INPUT, FORBIDDEN, your own codes) pass
// through, because the client is supposed to read them.
return formattedError;
},
}),
],
})
export class AppModule {}
Discussion