Validation & Error Handling
Validate input, handle errors gracefully, and write clean async Express routes
Validation
Error Handling
Async/Await
express-validator
1. Validation
What it is: The process of ensuring that incoming data (from requests, forms, APIs) meets expected formats, types, and business rules before processing it. Validation prevents bad data from corrupting your database, causing security issues, or breaking your application logic.
Why it matters
- Security: Prevents injection attacks (SQL, NoSQL, XSS)
- Data integrity: Stops garbage data from entering your system
- User experience: Provides clear, immediate feedback
- Compliance: Enforces business rules (e.g., age > 18, email format)
Types of validation
- Client-side: Fast feedback, but can be bypassed (UX enhancement only)
- Server-side: Mandatory security layer (never trust client input)
Common validation rules
Required fields, email format, min/max length, number ranges, patterns (regex), custom business logic.
Short Example (Express + express-validator):
JavaScript — express-validatorconst { body, validationResult } = require('express-validator');
// User registration with validation
app.post('/register',
// Validation rules
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Must be a valid email address'),
body('password')
.isLength({ min: 8 })
.matches(/[A-Z]/)
.withMessage('Password must be at least 8 chars with 1 uppercase'),
body('age')
.isInt({ min: 18, max: 120 })
.withMessage('Age must be between 18 and 120'),
body('username')
.trim()
.isAlphanumeric()
.isLength({ min: 3, max: 20 }),
(req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// If validation passes, proceed
res.json({ message: 'User created successfully' });
}
);
Example with manual validation (no library):
JavaScript — Manual validationfunction validateUser(data) {
const errors = [];
if (!data.email || !data.email.includes('@')) {
errors.push('Valid email is required');
}
if (!data.password || data.password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (data.age && (data.age < 18 || data.age > 120)) {
errors.push('Age must be 18-120');
}
return { isValid: errors.length === 0, errors };
}
2. Error Handling
What it is: A systematic approach to capturing, logging, and responding to errors in your application. Good error handling prevents crashes, provides useful debugging info (in development), and returns friendly messages (in production) without exposing sensitive internals.
Why it matters
- Prevents crashes: App keeps running even when things fail
- Debugging: Logs help track down issues
- Security: Doesn't leak stack traces or DB queries to clients
- User trust: Shows professional, helpful error messages
Error types to handle
- Synchronous errors: Try/catch blocks
- Asynchronous errors: Promise catches, error events
- Operational errors: Expected failures (invalid input, missing file)
- Programming errors: Bugs (undefined variables, type errors)
Best practices
- Use centralized error handling middleware (Express)
- Log errors (with Winston, Morgan, or similar)
- Distinguish between development (detailed) vs production (generic) responses
- Never expose stack traces to clients in production
Short Example (Express with error middleware):
JavaScript — Route with error handling// Basic error handling
app.get('/user/:id', async (req, res, next) => {
try {
const user = await db.findUser(req.params.id);
if (!user) {
const error = new Error('User not found');
error.statusCode = 404;
throw error;
}
res.json(user);
} catch (error) {
next(error); // Pass to error handling middleware
}
});
// Custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
JavaScript — Centralized error handler// Centralized error handler (after all routes)
app.use((err, req, res, next) => {
console.error(err.stack);
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Internal server error';
if (process.env.NODE_ENV === 'development') {
res.status(statusCode).json({
error: message,
stack: err.stack,
status: statusCode
});
} else {
res.status(statusCode).json({
error: message,
status: statusCode
});
}
});
// Example usage
app.get('/product/:id', async (req, res, next) => {
try {
if (!req.params.id) {
throw new AppError('Product ID required', 400);
}
const product = await db.getProduct(req.params.id);
if (!product) {
throw new AppError('Product not found', 404);
}
res.json(product);
} catch (err) {
next(err);
}
});
Unhandled rejections (Node.js)
JavaScript — Global error handlers// Catch unhandled promise rejections globally
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
server.close(() => process.exit(1));
});
// Catch uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
process.exit(1); // Mandatory cleanup
});
3. Async/Await
What it is: Syntactic sugar over Promises that makes asynchronous code look and behave more like synchronous code. Introduced in ES2017 (ES8), it eliminates callback hell and makes error handling with try/catch much cleaner.
Why it matters
- Readability: Linear, sequential code instead of nested callbacks
- Error handling: Use try/catch instead of
.catch() chains
- Debugging: Step-through debugging works naturally
- Maintainability: Easier to modify and understand async flows
How it works
async keyword before a function → function always returns a Promise
await keyword before a Promise → pauses execution until Promise settles
- Can only use
await inside async functions (or top-level in modern modules)
Common patterns
- Sequential execution: await one after another
- Parallel execution:
Promise.all() for independent operations
- Error handling: Wrap in try/catch blocks
- Conditional async: Use if statements with await
Basic async/await vs Promises:
JavaScript — Promises vs async/await// Promise version (nested)
function getUserData(userId) {
return fetchUser(userId)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.catch(error => console.error('Error:', error));
}
// Async/await version (cleaner)
async function getUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
} catch (error) {
console.error('Error:', error);
throw error;
}
}
Parallel execution with Promise.all:
JavaScript// Sequential (slower - 3 seconds total)
async function fetchSequential() {
const user = await fetchUser(); // 1 sec
const posts = await fetchPosts(); // 1 sec
const likes = await fetchLikes(); // 1 sec
return { user, posts, likes };
}
// Parallel (faster - 1 second total)
async function fetchParallel() {
const [user, posts, likes] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchLikes()
]);
return { user, posts, likes };
}
Error handling patterns:
JavaScript — Pattern 1: Simple try/catchasync function getUser(id) {
try {
const user = await db.users.findById(id);
return user;
} catch (error) {
console.error('Database error:', error);
return null;
}
}
JavaScript — Pattern 2: Specific error typesasync function transferMoney(from, to, amount) {
try {
await validateAccount(from);
await validateAccount(to);
await deductBalance(from, amount);
await addBalance(to, amount);
} catch (error) {
if (error.code === 'INSUFFICIENT_FUNDS') {
throw new Error(`Insufficient funds in account ${from}`);
} else if (error.code === 'ACCOUNT_LOCKED') {
throw new Error(`Account ${from} is locked`);
} else {
throw error;
}
}
}
JavaScript — Pattern 3: Async with retry logicasync function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
console.log(`Retry ${i + 1}/${retries} after error:`, error.message);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
Real Express route with async/await:
JavaScript — Express dashboard routeapp.get('/dashboard/:userId', async (req, res, next) => {
try {
const [profile, settings, notifications] = await Promise.all([
db.findProfile(req.params.userId),
db.findSettings(req.params.userId),
db.findUnreadNotifications(req.params.userId)
]);
const recentOrders = await db.findOrders(req.params.userId, { limit: 5 });
const recommendations = await generateRecommendations(recentOrders);
res.json({
profile,
settings,
notifications: notifications.length,
recommendations
});
} catch (error) {
next(error);
}
});
Common pitfalls to avoid
JavaScript — Pitfalls// Wrong: Forgetting await
async function getData() {
const result = db.query(); // Returns Promise, not data
return result;
}
// Correct: Use await
async function getData() {
const result = await db.query();
return result;
}
// Wrong: Using await outside async function
function process() {
const data = await fetchData(); // Syntax error
}
// Correct: Make function async
async function process() {
const data = await fetchData();
}
// Wrong: No error handling
async function risky() {
const data = await mightFail(); // Unhandled rejection if fails
}
// Correct: Always handle errors
async function safe() {
try {
const data = await mightFail();
return data;
} catch (error) {
console.error('Operation failed:', error);
return defaultValue;
}
}
Summary Table
| Concept |
Purpose |
Key Tools |
Anti-pattern |
| Validation |
Ensure data correctness/security |
express-validator, Joi, yup |
Trusting client-side validation |
| Error Handling |
Graceful failure & debugging |
try/catch, error middleware, Winston |
Empty catch blocks |
| Async/Await |
Clean asynchronous code |
async/await, Promise.all |
Forgetting await, nested try/catch |
Pro Tips
- Always validate on the server (never trust client input)
- Use custom error classes for different error types
- Combine async/await with
Promise.all for parallel operations
- Log errors with context (user ID, timestamp, request path)
- In production, return generic messages but log full details internally