Advanced

Error handling and validation

Real apps break. Learn to validate user input before it reaches the database, and to centralise error handling so crashes return clean pages instead of leaking details.

What you will see

User sees: Internal server error Log sees: column does not exist: bogus_col in users table

About 2 min of reading

Try It

Fail closed and fail safe

1

Fail closed: deny access when data is missing.

def require_token(headers): if not headers.get("Authorization"): return {"error": "Unauthorized", "code": 401} return {"ok": True} print() print("Error handling and validation") print("Missing token -> %s" % require_token({}))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Error handling and validation Missing token -> {'error': 'Unauthorized', 'code': 401}
If something is wrong, deny access. Never let missing data silently grant it.
2

Fail safe: hide internal details from the user.

try: raise RuntimeError("column does not exist: bogus_col in users table") except Exception as e: print("User sees: Internal server error") print("Log sees: %s" % e)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
User sees: Internal server error Log sees: column does not exist: bogus_col in users table
Return short, honest error messages to users and detailed ones to your logs.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

Input is checked before use; if it fails, a 400 is returned. Otherwise the request proceeds, and any thrown error is caught by the central handler.
1 A request arrives with user input
↓
2 The handler validates each field
↓
3 Bad input returns a 400 immediately
↓
4 Good input proceeds to the logic
↓
5 An error calls next(err)
↓
6 The central handler logs and replies
Why this works
Fail closed, fail safe

Two rules keep an app from hurting people:

  • **Fail closed*: if something is wrong, deny access. Never let missing data silently grant it.
  • **Fail safe*: never expose internal details. A stack trace tells an attacker how your app is built.

Return short, honest error messages to users and detailed ones to your logs. This split protects both the user and the app, and it is a habit that separates shipped apps from experiments.

Examples

Validating input

Always check that input looks right before you use it.

function isEmail(value) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}

app.post('/signup', (req, res) => {
  const { email, password } = req.body;
  if (!isEmail(email) || !password) {
    return res.status(400).send('Email and password are required');
  }
  // safe to continue
});

A bad email or a missing field is caught at the door. This keeps garbage out of your database and gives users a clear message about what went wrong.

A central error handler

Express can jump to one error handler if you call next(err). Put it last so it catches everything.

app.use((err, req, res, next) => {
  console.error(err.message);
  res.status(500).send('Something went wrong');
});

Inside a route, throw an error or call next(err) to trigger it. This way bugs surface in one place, your real errors are logged, and users never see a technical stack trace.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Authentication and passwords
Previous Lesson
Authentication and passwords
Next Lesson
Building a REST API
Building a REST API →