Error handling is crucial in backend development. Even well-written APIs can encounter issues, such as invalid data from users, non-existent resources, or failures from external services like databases. Effective error handling allows applications to respond with clear and consistent messages, rather than crashing or revealing sensitive information.
Good backend systems manage errors gracefully by using meaningful messages and appropriate HTTP status codes. Thus, understanding error handling is essential for creating reliable and professional APIs.
Today, for Day 43, the goal was to understand how error handling works and how errors are efficiently handled in the backend.
Types of Errors in Backend Applications
In backend development, errors usually fall into three main categories.
Understanding these helps you design clean API responses and debugging strategies.
1. Operational Errors
These are expected errors during normal application usage.
Examples:
User not found
Invalid password
Validation failed
Unauthorized access
These errors are not bugs.
They should return clean and meaningful responses to the client.
Example API response:
{
"success": false,
"message": "User not found"
}
Common operational errors:
- Invalid input
- Authentication failure
- Authorization issues
- Missing resources
2. Programming Errors
These are developer mistakes (bugs).
Examples:
undefined variable
incorrect logic
null reference
These indicate something is wrong in the code itself.
Programming errors usually require fixing the code, not returning them directly to users.
3. System Errors
System errors come from external systems or infrastructure.
Examples:
database connection failure
file system error
network timeout
third-party API failure
These are often outside your application's control.
Good APIs handle them gracefully without crashing the server.
Default Express Error Handling
In Express, errors must be passed to the next middleware using next().
Example:
app.get("/user", (req, res, next) => {
try {
throw new Error("Something went wrong");
} catch (error) {
next(error);
}
});
Calling next(error) tells Express:
"An error occurred — send it to the error handling middleware."
Global Error Handling Middleware
Instead of handling errors inside every route, production APIs use a centralized error handler.
Example:
app.use((err, req, res, next) => {
res.status(500).json({
success: false,
message: err.message
});
});
Important concept:
Error middleware must have four parameters:
(err, req, res, next)
This signature tells Express that this middleware is specifically for handling errors.
Standardized Error Responses
Good APIs always return consistent error responses.
Example structure:
{
"success": false,
"message": "Resource not found"
}
Or:
{
"success": false,
"error": "Unauthorized access"
}
Best practices:
- consistent structure
- meaningful error messages
- correct HTTP status codes
- no sensitive information in production
Common status codes:
| Status Code | Meaning |
|---|---|
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 500 | Internal Server Error |
Custom Error Classes
Instead of using the generic Error, we can create custom error classes.
Example:
class AppError extends Error {
constructor(message, statusCode) {
super(message)
this.statusCode = statusCode
}
}
Conceptual structure:
Error
└── AppError
Example usage:
throw new AppError("User not found", 404)
Benefits:
- attach HTTP status codes
- cleaner error handling
- better debugging
- consistent API responses
Async Problems in Express
Most modern backend controllers are asynchronous.
Example:
app.get("/users", async (req, res) => {
const users = await User.find()
res.json(users)
})
Problem in Express 4:
If an error occurs inside an async function, Express does not automatically catch it.
Example:
UnhandledPromiseRejectionWarning
This can crash your server if not handled properly.
(Note: Express 5 improves this behavior.)
The Try/Catch Problem
A common solution is using try/catch.
Example:
app.get("/users", async (req, res, next) => {
try {
const users = await User.find()
res.json(users)
} catch (error) {
next(error)
}
})
But repeating try/catch in every controller becomes messy and hard to maintain.
The Async Wrapper Pattern
A better solution is using an Async Wrapper.
Instead of writing try/catch everywhere, we create a reusable function.
Concept:
asyncWrapper(fn)
Usage:
asyncWrapper(getUsers)
Example:
function asyncWrapper(fn) {
return function(req, res, next) {
fn(req, res, next).catch(next)
}
}
The wrapper automatically catches errors and forwards them to the global error handler.
Benefits:
- cleaner controllers
- reusable logic
- widely used in production APIs
How the Async Wrapper Works
Example implementation:
function asyncWrapper(fn) {
return function (req, res, next) {
Promise.resolve(fn(req, res, next)).catch(next)
}
}
Example usage:
router.get("/users", asyncWrapper(getUsers))
Now your controller can stay clean:
const getUsers = async (req, res) => {
const users = await User.find()
res.json(users)
}
No try/catch needed.
Error Flow in a Professional API
In well-structured APIs, the error flow typically looks like this:
Route
↓
Async Wrapper
↓
Controller
↓
Error Occurs
↓
Global Error Handler
↓
Client Response
This architecture keeps the backend code:
- clean
- scalable
- maintainable
🧠 Key Takeaways
Error handling is a core skill in backend development.
Important concepts to remember:
- understand operational vs programming errors
- use global error middleware
- create custom error classes
- avoid repeating
try/catch - use async wrapper pattern
Learning backend is not just about making APIs work. It's about making them reliable, maintainable, and production-ready. Error handling is one of the foundations of professional backend development. These patterns are used in real production Node.js APIs.
Thanks for reading. Feel free to share your thoughts!
Top comments (0)