Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
22 Aug 2025 · 6 min read ·Article 53 / 125
Go

53 Logging and Error Handling in a GraphQL Server

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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 LevelDescription
ErrorErrors that cause a request to fail
WarningThings worth paying attention to, but not fatal
InfoGeneral information about how the system is running
DebugTechnical 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:

json
 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

bash
1npm install apollo-server winston

Configuring Winston

js
 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:

js
 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:

js
 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:

js
 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}
As a result, the 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:

json
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:

json
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:

js
 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:

MERMAID
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:

  1. 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.

  2. Log enough context when an error occurs, but never log passwords/API keys!

  3. Use clear error codes/enums in extensions.code

  4. Add a correlation id to every request for tracing

  5. Notify ops/dev when a critical error appears (e.g. integrate with Slack, Sentry, or email)

  6. 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!

Related Articles

💬 Comments