GraphQL Async Resolvers and Databases

Fetching data from a database takes time — it is an asynchronous operation. GraphQL resolvers support Promises natively, so you write async resolvers the same way you write any async JavaScript function. GraphQL waits for each Promise to resolve before sending the final response.

Synchronous vs Asynchronous Resolvers

  Synchronous (in-memory data):     Asynchronous (database):
  ──────────────────────────────    ──────────────────────────
  book: () => {                     book: async () => {
    return books[0];                  const result = await db.query(
  }                                     'SELECT * FROM books LIMIT 1'
                                      );
                                      return result.rows[0];
                                    }

GraphQL detects whether a resolver returns a plain value or a Promise. If it returns a Promise, GraphQL waits for it to resolve before moving to the next step.

Connecting to a Real Database (PostgreSQL Example)

  npm install pg

  // db.js — database connection pool
  import pg from 'pg';
  const { Pool } = pg;

  export const pool = new Pool({
    host:     'localhost',
    database: 'myapp',
    user:     'postgres',
    password: 'secret',
    port:     5432,
  });

  // Pass pool through context
  await startStandaloneServer(server, {
    context: async () => ({ db: pool })
  });

  // Resolver uses context.db
  const resolvers = {
    Query: {
      users: async (_, __, context) => {
        const result = await context.db.query('SELECT * FROM users');
        return result.rows;
      },
      user: async (_, args, context) => {
        const result = await context.db.query(
          'SELECT * FROM users WHERE id = $1',
          [args.id]
        );
        return result.rows[0] || null;
      }
    }
  };

Connecting to MongoDB

  npm install mongoose

  // models/Book.js
  import mongoose from 'mongoose';
  const BookSchema = new mongoose.Schema({
    title:  String,
    author: String,
    year:   Number,
  });
  export const Book = mongoose.model('Book', BookSchema);

  // Resolvers with Mongoose
  const resolvers = {
    Query: {
      books: async () => {
        return await Book.find({});        // Returns all books
      },
      book: async (_, args) => {
        return await Book.findById(args.id); // Returns one book
      }
    },
    Mutation: {
      addBook: async (_, args) => {
        const book = new Book({
          title:  args.title,
          author: args.author,
          year:   args.year,
        });
        return await book.save();           // Saves and returns new book
      }
    }
  };

Error Handling in Async Resolvers

Wrap async resolver logic in try-catch to handle database failures gracefully. Throwing an error inside a resolver causes GraphQL to add it to the errors array in the response.

  user: async (_, args, context) => {
    try {
      const user = await context.db.query(
        'SELECT * FROM users WHERE id = $1', [args.id]
      );
      if (!user.rows[0]) {
        throw new Error('User not found');
      }
      return user.rows[0];
    } catch (err) {
      console.error('Database error:', err);
      throw new Error('Failed to fetch user');
    }
  }

Parallel Execution of Sibling Resolvers

When two fields at the same level are independent, GraphQL starts both their resolvers at the same time. This parallelism speeds up response time when you have multiple database calls that do not depend on each other.

  Query: { products { name }  categories { name } }

  products resolver  starts ─┐
                              ├── Both run at the same time
  categories resolver starts ─┘

  Total time ≈ max(products time, categories time)
  Not: products time + categories time

Key Points

  • Resolvers return Promises and GraphQL waits for them automatically — use async/await freely.
  • Pass the database client through context so every resolver can access it without importing directly.
  • Throwing an error inside a resolver adds it to the response errors array.
  • Independent sibling resolvers run in parallel, reducing total response time.

Leave a Comment