Validation & Error Handling

Validate input, handle errors gracefully, and write clean async Express routes

Validation Error Handling Async/Await express-validator

Table of Contents

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