⇦ Back

WDX-180

Web Development X

Validation, Error Handling & Defensive Programming

Building Applications That Survive Real Users

Beginners write code for ideal users.

Professionals write code for actual users.

Actual users are chaos in human form.

Until now we’ve assumed users submit:

  Valid Names
  Valid Prices
  Valid Emails

Reality looks more like:

  Price = banana

  Email = not-an-email

  Name = ""

Or:

  Price = -999999999

Or:

  Name = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...

(3 million characters later)

Today’s lesson is about building systems that don’t immediately collapse when exposed to the public internet.

Learning Objectives

By the end of this lesson, students will be able to:

Part 1 — Never Trust User Input

Rule #1:

  Everything from the client
  is untrusted.

EVERYTHING.

Including:

  Forms

  Cookies

  Query Parameters

  Route Parameters

  Headers

Assume every value is:

  Wrong

  Malicious

  Missing

until proven otherwise.

Part 2 — Types of Validation

Example Product:

  {
      name: "Keyboard",
      price: 89.99
  }

Possible checks:

Required

  Name required

Length

  Max 255 chars

Numeric

  Price must be number

Range

  Price > 0

Format

  Valid email

Different validations solve different problems.

Part 3 — The Validation Layer

❌ Bad:

  router.post('/products/create',
      (req, res) => {

          if (...) {}

          if (...) {}

          if (...) {}

          if (...) {}

      }
  );

Route becomes:

  Validation

  Database

  Rendering

  Business Logic

all mixed together.

Messy.

Better:

  validateProduct(req.body);

Separation of concerns.

Part 4 — Building a Validator

Example:

  function validateProduct(data) {
      const errors = [];
      if ( !data.name ) {
          errors.push('Name required');
      }
      return errors;
  }

Usage:

  const errors = validateProduct(req.body);

Check:

  if ( errors.length ) {
      return res.render('products/create',
          {
            title: "Create Product",
            errors
          }
      );
  }

Simple.

Predictable.

Testable.

Your errors will end up as data into the template, so you can iterate over them using EJS and display them to the user:

  <% if ( typeof errors !=="undefined" ) { %>
    <% errors.forEach( error => { %>
      <div class="error">
        <%= error %>
      </div>
    <% }); %>
  <% } %>  

Part 5 — Validating Product Names

Example:

  if ( name.length < 3 ) {
      errors.push('Name too short');
  }

Example:

  if ( name.length > 255 ) {
      errors.push('Name too long');
  }

Protects against:

  Empty names

  Massive payloads

Part 6 — Validating Prices

Current:

  price = "banana"

Convert:

  const numericPrice = Number(price);

Validate:

  if ( Number.isNaN(numericPrice) ) {
      errors.push('Invalid price');
  }

Range:

  if ( numericPrice <= 0 ) {
      errors.push('Price must be positive');
  }

Part 7 — Email Validation

Simple check:

  email.includes('@')

Works poorly.

Better:

  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

Validate:

  if ( !emailRegex.test( email ) ) {
      errors.push( 'Invalid email' );
  }

Not perfect.

Much better.

Part 8 — Server Errors vs User Errors

These are different.

User Error:

  Invalid Email

User can fix it.

Server Error:

  Database Offline

User cannot.

Response:

  400 Bad Request

for user problems.

Response:

  500 Internal Server Error

for server problems.

Important distinction.

HTTP STATUS 4xx -> Your mistake.

HTTP STATUS 5xx -> Our mistake.

Part 9 — Understanding try/catch

Example:

  try {
      const product = createProduct();
  } catch(error) {
      console.error( error );
  }

Without try/catch application may crash.

With try/catch application can recover.

Part 10 — Express Error Middleware

Express error handlers are special middleware with 4 parameters (not 3).

When an error is thrown or passed to next(error), Express skips regular middleware and routes and jumps directly to the error handler.

Error handler must have exactly 4 parameters:

  function errorHandler( err, req, res, next ) {
      console.error(err.stack);
      res.status(500).render('500');
  }

Critical: Register error handlers after all other middleware and routes:

  // Regular routes
  app.get('/', (req, res) => {
      res.send('Home');
  });

  // Other middleware
  app.use(express.json());

  // Error handler registered LAST
  app.use(errorHandler);

Example with multiple error types:

  function errorHandler( err, req, res, next ) {
      console.error(err.stack);

      // Default error
      let status = 500;
      let message = 'Internal Server Error';

      // Handle specific errors
      if (err instanceof ValidationError) {
          status = 400;
          message = err.message;
      } else if (err instanceof NotFoundError) {
          status = 404;
          message = 'Resource not found';
      }

      res.status(status).render('error', { 
          message: message,
          error: process.env.NODE_ENV === 'development' ? err : {} 
      });
  }

  app.use(errorHandler);

To trigger error handler, either throw or use next():

  app.get('/product/:id', (req, res, next) => {
      const product = findProduct(req.params.id);
      
      if (!product) {
          // Trigger error handler
          return next(new NotFoundError('Product not found'));
      }

      res.json(product);
  });

Every unhandled error will arrive at the error handler.

Part 11 — Custom Error Classes

Instead of:

  throw new Error('Not Found');

Create:

  class NotFoundError extends Error {}

Usage:

  throw new NotFoundError('Product Missing');

Handler:

  if ( err instanceof NotFoundError ) {
      return res.status(404).render('404');
  }

Cleaner.

More maintainable.

Part 12 — Operational vs Programmer Errors

A critical distinction.

Operational Error:

  Database unavailable

  Missing file

  Network failure

Expected eventually.

Handle gracefully.

Programmer Error:

  user.name.toUpperCase()

when:

  user === undefined

Bug.

Needs fixing.

Understanding the difference helps with debugging and monitoring.

Part 13 — Reusable Validation Middleware

Instead of:

  validateProduct()

inside every route.

Middleware:

  function validateProductMiddleware( req, res, next ) {
      const errors = validateProduct( req.body );
      if ( errors.length ) {
          return res.render('form', { errors });
      }

      next();

  }

Route:

  router.post('/products/create',
      validateProductMiddleware,
      createProduct
  );

Very scalable.

Part 14 — Preserving User Input

Validation fails.

❌ Bad:

  Everything disappears

✅ Good:

  res.render('form',
      {
          product: req.body, // Previously submitted data are retained
          errors
      }
  );

Input:

  value="<%= product.name %>"

Professional UX.

Part 15 — Logging Errors

Never do:

  catch(error) {

  }

Silent failures are terrible.

Always log:

  console.error( error );

Later you’ll replace this with:

But logging is the beginning.

Part 16 — Defensive Programming

Assume:

  Everything fails eventually.

Examples:

  Database

  Network

  File Upload

  User Input

  Third-party APIs

Write code that anticipates failure.

Not because you’re pessimistic.

Because you’re realistic.

Common Beginner Mistakes

Validating Only in the Browser

Bad:

  required

HTML attribute only.

Attackers bypass browsers.

ALWAYS Validate on the server-side.

Giant Route Files

Separate:

  Validation

  Database

  Rendering

into dedicated layers.

Returning Generic Errors

❌ Not helpful:

  Something went wrong

✅ Helpful:

  Email required

Ignoring Errors

Every error should have a plan.

Catching Everything and Hiding It

Users need useful messages.

Developers need useful logs.

Reading Time

Bonus Challenge

Create:

  Validation Library

for your project.

Example:

  validators/
    ├── product.js
    ├── user.js
    ├── auth.js

Usage:

  const errors = productValidator(req.body);

You now have the beginnings of a reusable application framework.

Key Takeaways

Today you learned:

At this point, your CMS is no longer merely functional—it is becoming resilient. The difference between a hobby project and a production application is often not the happy path. It’s how the system behaves when everything goes wrong.


⚠️ A large part of the content of this module was created using Generative AI (ChatGPT). The synthetic (AI-generated) content was reviewed and curated by Kostas Minaidis.


Project maintained by in-tech-gration Hosted on GitHub Pages — Theme by mattgraham