35 Handling Database Errors in Resolvers
35 Handling Database Errors in Resolvers
Error handling is a fundamental aspect of building back-end applications, especially at the resolver layer in GraphQL or REST APIs. A resolver acts as the bridge between user requests and the data processed in the database. Unfortunately, interacting with a database is highly prone to all kinds of errors—from SQL syntax errors, connection timeouts, to data constraint violations. In this article, I’ll share 35 ways to handle database errors in resolvers, complete with code examples, simulations, and flow diagrams so that the product we build becomes increasingly robust.
Why Is Database Error Handling Important?
Failing to handle database errors can have serious consequences:
- Data corruption: if a transaction isn’t rolled back.
- Service downtime: for example, when a failed database connection isn’t managed properly.
- Poor user experience: error messages that aren’t informative, or worse, that leak internal details.
- Security leaks: a stray stacktrace ending up all the way in the frontend.
Types of Errors That Arise in Databases
The following table illustrates several types of database errors along with their examples:
| Error Category | Example Error Code/Sub-Code | Example Case |
|---|---|---|
| Syntax Error | 42601 (Postgres) | Typo in an SQL query (SELECT * FRM users;) |
| Connection Error | ECONNREFUSED, timeout | Database down/service hanging |
| Data Constraint | unique_violation (23505) | Inserting a user email that already exists, with a unique email field |
| Deadlock | deadlock_detected (40P01) | Two queries waiting on each other |
| Not Found | empty result set | A query searching for data that has already been deleted |
| Authorization Error | insufficient_privilege (42501) | The DB user doesn’t have read access to a certain table |
Error Handling Flow Diagram in the Resolver
Let’s look at a simple flow diagram of what happens when our query executes in a resolver and encounters an error:
flowchart TD
A[Client Request] --> B{Resolver}
B --> C{DB Query}
C -- Success --> D[Return Data]
C -- Error --> E{Type of Error}
E -- Connection/Timeout --> F[Log & Return 503]
E -- Not Found --> G[Return Null / NotFound Err]
E -- Constraint --> H[Return 400/409 & Custom Error]
E -- Unknown --> I[Log Detail & Return 500]
F --> J[Client]
G --> J
H --> J
I --> J
35 Patterns for Handling Database Errors in Resolvers
1. Use a Try-Catch Block
1async function resolver(_, args, context) {
2 try {
3 return await db.query('SELECT * FROM ...');
4 } catch (err) {
5 console.error('DB Error:', err);
6 throw new Error('Internal Server Error');
7 }
8}2. Map Error Codes to Appropriate Messages
Create a map from DB error codes to API messages.
1const errorMap = {
2 '23505': 'Duplicate entry', // Unique constraint
3 '42601': 'Syntax Error',
4};
5
6catch (err) {
7 const message = errorMap[err.code] || 'Internal Error';
8 throw new Error(message);
9}3. Use a Custom Error Class
1class NotFoundError extends Error {}
2class ValidationError extends Error {}4. Complete Yet Safe Error Logging
Log the full message on the server, but show a concise version to the client.
5. Retry on Connection Errors
1async function queryWithRetry() {
2 for (let i = 0; i < 3; i++) {
3 try return await db.query(...);
4 catch (err) {
5 if (isConnectionError(err)) await sleep(100);
6 else throw err;
7 }
8 }
9 throw new Error('Cannot connect to DB');
10}6. Roll Back the Transaction on Error
1try {
2 await db.begin();
3 await db.query('...');
4 await db.commit();
5} catch (e) {
6 await db.rollback();
7 throw e;
8}7. Standardize the Error Response
Define an error format:
1{
2 "code": "CONFLICT",
3 "message": "Duplicate record",
4 "details": "users.email must be unique"
5}8. Separate Internal and User Errors
Internal DB errors should never “leak” to the client.
9. Use Error Boundaries in the Service/Use Case Layer
10. Validate Input Data Before Querying
11. Graceful Fallback for Not Found
1if (result.rows.length === 0) return null;
12. Return Null Along With an Error Message Per the GraphQL Spec
13. Use an Error Handling Library
Such as Boom for REST.
14. Use a Deadlock/Timeout Detector
15. Implement Exponential Backoff on Retries
16. Document the Errors That Occur
Keep a log of DB error codes and their mappings in your documentation.
17. Be Consistent With HTTP Codes
409 for duplicates, 404 when not found, 500 for internal errors.
18. Prevent SQL Injection (input validation & parameterized queries)
19. Never Show the Stacktrace to the Client
20. Store Errors in an Error Monitoring Tool
Such as Sentry or Datadog.
21. Show User-Friendly Error Messages
For example: “Email is already in use”, not “23505 unique_violation”.
22. Test the Error Path With Unit Tests
23. Make Sure the DB Connection Is Always Closed
Using pooling/auto-close.
24. Handle Query Timeouts
25. Ensure Transactions Are Atomic
26. Use the Right Isolation Level for Transactions
27. Use Localized DB Error Messages (locale messages)
To make them more user-friendly.
28. Rate Limit Repeated Query Errors
To avoid DDoS/consistently clogging the server.
29. Blacklist/Ban Abusive Error-Generating IPs
30. Handle DB Errors Across Multiple Shards/Microservices
31. Build Dedicated Error Metrics & Health Checks for the DB
32. Replay the Request if It’s Idempotent
33. Avoid Redundant Queries When the DB Is Degraded
34. Use CQRS/Read-Write Split Under High Load
35. Audit-Log All Important Errors for Investigation
Simulation (Node.js/Express + PostgreSQL)
Let’s simulate handling a unique error: a duplicate key.
1const { Pool } = require('pg');
2const db = new Pool();
3
4async function createUser(email) {
5 try {
6 await db.query('INSERT INTO users(email) VALUES($1)', [email]);
7 return { success: true };
8 } catch (err) {
9 if (err.code === '23505') {
10 // Unique violation
11 throw new Error('Email is already registered');
12 }
13 // Log error detail
14 console.error('[DB]', err);
15 throw new Error('An error occurred while creating the user');
16 }
17}Conclusion
Handling database errors in resolvers isn’t just about writing a “catch” in every function—it’s about integrating best practices at each layer, documenting errors, and tailoring the level of detail in messages to each audience (dev vs. user). With the 35 strategies above, you can strengthen your application against database errors, from small scale all the way to enterprise.
Don’t hesitate to adapt and combine the techniques above so that your error handling stays scalable and easy to debug.
Happy coding, and may errors never haunt your production!