Loading

Quipoin Menu

Learn • Practice • Grow

express-js / ExpressJS Error Handling Debugging & Best Practices
tutorial

ExpressJS Error Handling Debugging & Best Practices

Imagine you're driving a car. When something goes wrong, a warning light appears on the dashboard telling you what's wrong. In Express applications, error handling is like that dashboard – it catches problems and tells you (and your users) what went wrong. Good error handling separates professional applications from amateur ones.

Types of Errors in Express

  • Operational Errors – Expected errors like invalid input, database connection failure, file not found.
  • Programming Errors – Bugs in code like undefined variables, syntax errors, incorrect logic.

Basic Error Handling with try/catch
app.get('/user/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

Centralized Error Handling Middleware
Instead of try/catch in every route, you can create a centralized error handler.
// Custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}

// Async wrapper to catch errors
const catchAsync = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};

// Route using async wrapper
app.get('/user/:id', catchAsync(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new AppError('User not found', 404);
}
res.json(user);
}));

// 404 handler for unmatched routes
app.use((req, res, next) => {
next(new AppError(`Can't find ${req.originalUrl} on this server`, 404));
});

// Global error handling middleware
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';

// Development vs Production error response
if (process.env.NODE_ENV === 'development') {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
} else {
// Production: don't leak error details
res.status(err.statusCode).json({
status: err.status,
message: err.isOperational ? err.message : 'Something went wrong'
});
}
});

Handling Specific Error Types
// MongoDB duplicate key error
const handleDuplicateKeyError = (err) => {
const field = Object.keys(err.keyPattern)[0];
const message = `${field} already exists. Please use another value.`;
return new AppError(message, 400);
};

// MongoDB validation error
const handleValidationError = (err) => {
const errors = Object.values(err.errors).map(el => el.message);
const message = `Invalid input data: ${errors.join('. ')}`;
return new AppError(message, 400);
};

// JWT errors
const handleJWTError = () =>
new AppError('Invalid token. Please log in again.', 401);

const handleJWTExpired = () =>
new AppError('Your token has expired. Please log in again.', 401);

Debugging Techniques

1. Using console.log (Basic)
console.log('Value:', variable);
console.log('Request body:', req.body);
console.log('Error:', error);

2. Using Debug Module (Professional)
npm install debug
const debug = require('debug')('app:server');
const debugDb = require('debug')('app:database');

debug('Server starting on port 3000');
debugDb('Connected to MongoDB');
Run with: DEBUG=app:* node app.js

3. Using Node.js Inspector
node --inspect app.js
Then open Chrome and go to chrome://inspect

Best Practices for Express Applications

1. Project Structure
src/
├── controllers/ # Route handlers
├── models/ # Database models
├── routes/ # Route definitions
├── middleware/ # Custom middleware
├── utils/ # Helper functions
├── config/ # Configuration files
├── services/ # Business logic
├── validators/ # Input validation
└── app.js # App entry point

2. Environment Variables
// config/config.js
require('dotenv').config();

module.exports = {
port: process.env.PORT || 3000,
dbUrl: process.env.DATABASE_URL,
jwtSecret: process.env.JWT_SECRET,
nodeEnv: process.env.NODE_ENV
};

3. Input Validation
const { body, validationResult } = require('express-validator');

app.post('/register',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Proceed
}
);

4. Logging with Morgan
npm install morgan
const morgan = require('morgan');

if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}

5. Use HTTP Status Codes Correctly
  • 200 – OK (success)
  • 201 – Created (POST success)
  • 400 – Bad Request (client error)
  • 401 – Unauthorized (not authenticated)
  • 403 – Forbidden (authenticated but not allowed)
  • 404 – Not Found
  • 500 – Internal Server Error

Two Minute Drill
  • Use centralized error handling with a global error middleware.
  • Create custom error classes for different error types.
  • Use catchAsync wrapper to avoid try/catch in every route.
  • Differentiate between development and production error responses.
  • Use debug module or morgan for logging.
  • Organize code with proper project structure.
  • Always validate input and use environment variables.

Need more clarification?

Drop us an email at career@quipoinfotech.com