GraphQL with NestJS
Integrating GraphQL into a NestJS application using Apollo Server or other GraphQL libraries.
GraphQL Error Handling in NestJS
This document outlines strategies for implementing robust error handling in a GraphQL API built with NestJS. We'll focus on using custom error types and exception filters to provide informative error messages to clients.
Why Error Handling is Important in GraphQL
GraphQL error handling differs from traditional REST APIs. GraphQL returns a data
and errors
field in the response, even for successful requests that may contain partial errors. This requires a structured approach to managing and reporting errors in your resolvers and services.
Core Concepts
1. Custom Error Types
Defining custom error types allows you to categorize and provide specific information about different error scenarios. This makes it easier for clients to handle errors gracefully.
Example (GraphQL Schema):
type User {
id: ID!
name: String!
email: String!
}
union UserResult = User | UserNotFoundError | DatabaseError
type UserNotFoundError {
message: String!
}
type DatabaseError {
message: String!
}
type Query {
user(id: ID!): UserResult
}
In this example, UserResult
can be either a User
object, a UserNotFoundError
, or a DatabaseError
. The client can then inspect the __typename
field on the error object to determine the specific error type.
2. Exception Filters
Exception filters in NestJS are used to catch exceptions thrown within your application and transform them into GraphQL-compatible error responses. This provides a centralized location for error handling logic.
Example (NestJS Exception Filter):
import { Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { GqlExceptionFilter, GqlArgumentsHost } from '@nestjs/graphql';
@Catch()
export class AllExceptionsFilter implements GqlExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const gqlHost = GqlArgumentsHost.create(host);
console.error(exception); // Log the error for debugging
if (exception instanceof HttpException) {
// Handle HTTP Exceptions (e.g., 404, 500)
return new Error(exception.message); // Or create a custom GraphQL error type
} else if (exception instanceof UserNotFoundError) {
// Handle custom UserNotFoundError
return new Error(exception.message);
} else if (exception instanceof DatabaseError) {
// Handle custom DatabaseError
return new Error(exception.message);
} else {
// Generic error handling
return new Error('Internal server error');
}
}
}
export class UserNotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'UserNotFoundError'; // Important for type checking
Object.setPrototypeOf(this, UserNotFoundError.prototype); // for extending Error in ES5
}
}
export class DatabaseError extends Error {
constructor(message: string) {
super(message);
this.name = 'DatabaseError';
Object.setPrototypeOf(this, DatabaseError.prototype); // for extending Error in ES5
}
}
Explanation:
- The
@Catch()
decorator specifies which exceptions this filter handles. Using an empty@Catch()
catches all exceptions. GqlArgumentsHost
is used to access GraphQL-specific context, such as the resolver arguments.- The
catch
method receives the exception and the arguments host. - Inside the
catch
method, we check the type of the exception. If it's anHttpException
(thrown by NestJS's built-in HTTP exception handling), we can extract the error message. We also handle our custom error types. - The filter transforms the exception into a GraphQL error. In this example, we're simply creating a generic
Error
object, but you'll likely want to map your custom error types to corresponding GraphQL error objects. - It's crucial to log exceptions (
console.error(exception)
) for debugging and monitoring purposes.
Registering the Exception Filter:
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { APP_FILTER } from '@nestjs/core';
import { AllExceptionsFilter } from './all-exceptions.filter';
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
}),
],
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}
This registers the AllExceptionsFilter
globally, ensuring that it catches all unhandled exceptions within your GraphQL API.
3. Throwing Errors in Resolvers
Within your resolvers, throw appropriate exceptions when errors occur. Use the custom error classes you've defined.
Example (Resolver):
import { Resolver, Query, Args } from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './user.model';
import { UserNotFoundError, DatabaseError } from './all-exceptions.filter';
@Resolver(() => User)
export class UserResolver {
constructor(private readonly userService: UserService) {}
@Query(() => User, { nullable: true })
async user(@Args('id') id: string): Promise<User | UserNotFoundError | DatabaseError | null> {
try {
const user = await this.userService.findUserById(id);
if (!user) {
throw new UserNotFoundError(`User with ID ${id} not found`);
}
return user;
} catch (error) {
console.error('Error fetching user:', error);
if (error instanceof UserNotFoundError) {
throw error; //Re-throw UserNotFoundError
}
// Differentiate between known and unknown errors for better error handling.
if (error instanceof Error && error.message.includes('database connection')) {
throw new DatabaseError("Error connecting to the database.");
}
throw new DatabaseError('Failed to fetch user from database'); // Wrap generic database errors
}
}
}
Explanation:
- The resolver calls a service (
UserService
) to fetch a user. - If the user is not found, a
UserNotFoundError
is thrown. - Any other errors during the database operation are caught and wrapped in a
DatabaseError
(or re-thrown if it's already the custom error). - Re-throwing UserNotFoundError allows the exception filter to handle it.
4. The UserService
The service interacts with the database or other data sources. It's responsible for handling data access errors and potentially throwing custom error types.
Example (UserService):
import { Injectable } from '@nestjs/common';
import { User } from './user.model';
@Injectable()
export class UserService {
async findUserById(id: string): Promise<User | null> {
// Simulate database interaction
const users = [
{ id: '1', name: 'John Doe', email: 'john.doe@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane.smith@example.com' },
];
const user = users.find(u => u.id === id);
if (!user) {
return null;
}
return user;
}
}
Best Practices
- Log all errors: Logging errors is crucial for debugging and monitoring your application.
- Use specific error messages: Provide informative error messages that help clients understand the nature of the error.
- Avoid exposing sensitive information: Don't include sensitive data in error messages that might be exposed to clients.
- Centralize error handling: Use exception filters to centralize your error handling logic.
- Consider using a dedicated error tracking service: Tools like Sentry can help you track and manage errors in your application.
- Implement client-side error handling: Handle errors gracefully on the client-side and provide helpful feedback to the user.
Example Client Response (with Error)
If the client requests a user with an ID that doesn't exist, the GraphQL server might return a response like this:
{
"data": {
"user": null
},
"errors": [
{
"message": "User with ID non_existent_id not found",
"locations": [
{
"line": 7,
"column": 3
}
],
"path": [
"user"
],
"extensions": {
"code": "USER_NOT_FOUND", // Optionally, you can add a code
"stacktrace": [ // Consider if you want to send the stacktrace.
"..."
]
}
}
]
}
The client can then use the message
and optionally the custom code
in extensions
to handle the error appropriately.