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/awaitfreely. - 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
errorsarray. - Independent sibling resolvers run in parallel, reducing total response time.
