53 Logging and Error Handling in a GraphQL Server
53 Logging and Error Handling in a GraphQL Server
One of the challenges in building modern APIs is ensuring observability and robust error handling. When we build a GraphQL server, whether with Node.js, Python, or any other language, good logging and error handling are not just nice-to-haves; they are a fundamental part that must not be overlooked. In this article, we will explore logging and error handling on a GraphQL server in depth, complete with code examples, simulations, and flow diagrams.
Why Are Logging and Error Handling Important?
Logging gives us visibility into what is happening in the system, helping with debugging, performance monitoring, and even attack detection. Good error handling ensures the application stays resilient and that users get relevant error messages without compromising the security of the system.
Core Concepts of Logging & Error Handling in GraphQL
Logging
Simply put, logging is the process of recording events during program execution. On a GraphQL server, we can split logging into several levels, for example:
| Log Level | Description |
|---|---|
| Error | Errors that cause a request to fail |
| Warning | Things worth paying attention to, but not fatal |
| Info | General information about how the system is running |
| Debug | Technical details, typically used during development |
Error Handling
GraphQL has a mechanism for error propagation that differs from REST. Every GraphQL response returns HTTP 200, even when an error occurs; the error details are placed in the errors field of the JSON response.
Example of a GraphQL error response:
1{
2 "data": null,
3 "errors": [
4 {
5 "message": "User not found",
6 "locations": [ { "line": 2, "column": 3 } ],
7 "path": [ "user" ]
8 }
9 ]
10}Implementing Logging in a GraphQL Server (Node.js Example)
Let’s start with a simple setup using Apollo Server and a popular logging library such as winston .
Installing the Packages
1npm install apollo-server winstonConfiguring Winston
1// logger.js
2const winston = require('winston');
3
4const logger = winston.createLogger({
5 level: 'info',
6 format: winston.format.json(),
7 transports: [
8 new winston.transports.Console(),
9 // new winston.transports.File({ filename: 'error.log', level: 'error' }),
10 ],
11});
12
13module.exports = logger;Logging in Resolvers
We want to log every time an error or an important event occurs, for instance a query for a specific user:
1// resolvers.js
2const logger = require('./logger');
3
4const resolvers = {
5 Query: {
6 user: async (_, { id }, ctx) => {
7 logger.info(`Fetching user with id=${id}`);
8 const user = await ctx.db.getUserById(id);
9 if (!user) {
10 logger.error(`User not found: id=${id}`);
11 throw new Error('User not found');
12 }
13 return user;
14 }
15 }
16};
17
18module.exports = resolvers;Logging Requests and Responses in Middleware
Apollo Server provides lifecycle hooks so we can log in a more structured way:
1// server.js
2const { ApolloServer } = require('apollo-server');
3const logger = require('./logger');
4const typeDefs = require('./typeDefs');
5const resolvers = require('./resolvers');
6
7const server = new ApolloServer({
8 typeDefs,
9 resolvers,
10 context: ({ req }) => ({ db: require('./db'), req }),
11 plugins: [
12 {
13 requestDidStart(requestContext) {
14 logger.info(`Received request: ${requestContext.request.query}`);
15 return {
16 willSendResponse({ response }) {
17 if (response.errors) {
18 logger.error(JSON.stringify(response.errors));
19 }
20 logger.info('Sending response...');
21 },
22 };
23 },
24 },
25 ]
26});
27
28server.listen().then(({ url }) => {
29 logger.info(`🚀 Server ready at ${url}`);
30});Error Handling in GraphQL
Using Custom Error Classes
GraphQL/Apollo provides the ApolloError class for customized errors, so we can set the status code and additional fields:
1const { ApolloError, UserInputError, AuthenticationError } = require('apollo-server');
2
3const resolvers = {
4 Mutation: {
5 updateProfile: async (_, args, ctx) => {
6 if (!ctx.user) {
7 throw new AuthenticationError('You must be logged in');
8 }
9 if (!args.email) {
10 throw new UserInputError('email is required');
11 }
12 // ...
13 try {
14 // Simulate db error
15 throw new Error('DB connection failed!');
16 } catch (err) {
17 throw new ApolloError('Internal server error', 'INTERNAL_ERROR');
18 }
19 }
20 }
21}extensions.code field in the error response will contain the corresponding code.Simulating the Error Output
Say we send the updateProfile mutation without being logged in; the output is:
1{
2 "data": null,
3 "errors": [{
4 "message": "You must be logged in",
5 "extensions": {
6 "code": "UNAUTHENTICATED"
7 }
8 }]
9}And if there is a database error:
1{
2 "data": null,
3 "errors": [{
4 "message": "Internal server error",
5 "extensions": {
6 "code": "INTERNAL_ERROR"
7 }
8 }]
9}Centralized Error Management
Instead of handling errors in every resolver, we can apply centralized error handling at the server layer with formatError:
1const server = new ApolloServer({
2 // ...
3 formatError: (err) => {
4 logger.error(`[GraphQL Error] ${err.message}`);
5 // Custom masking: only expose a safe message to the client
6 if (err.extensions.code === 'INTERNAL_SERVER_ERROR') {
7 return new Error('An unexpected error occurred'); // Hide internal details
8 }
9 return err;
10 }
11});Logging & Error Handling Flow (Mermaid Diagram)
Let’s look at the main flow of this process:
flowchart TD
A[Request Masuk] --> B{Ada Error?}
B -- Ya --> C{Error Known?}
C -- Ya --> D[Log Error w/ Context dan Return Custom Error]
C -- Tidak --> E[Log Error dan Masking Error, Return Generic Message]
B -- Tidak --> F[Log Sukses, Return Data]
In that diagram, we distinguish between errors whose type we already know (e.g. auth, input validation) and unexpected errors (runtime errors, db crashes, and so on).
Best Practices
From experience deploying GraphQL to production, here are a few recommendations you should follow:
Separate the handling of trusted vs untrusted errors
Trusted: domain errors, input, business logic.
Untrusted: network, database, runtime bugs — mask the error to the client and log the details on the server.Log enough context when an error occurs, but never log passwords/API keys!
Use clear error codes/enums in extensions.code
Add a correlation id to every request for tracing
Notify ops/dev when a critical error appears (e.g. integrate with Slack, Sentry, or email)
Do not expose stack traces or internal errors to the front-end
Closing
Logging and error handling in a GraphQL server is not just about adding a console.log and then throwing an error. A good implementation makes the system easy to monitor, secure, and easy to debug. Remember, a system will inevitably error out — what sets it apart is how prepared our application is to face it.
Keep sharpening your observability skills, because with a fast-growing ecosystem like GraphQL, robust error handling & logging is no longer optional. Happy hacking!