How Do I Fix an “Undefined” Error Despite Linking HTML Correctly

When I first ran into the dreaded agentData is not defined error, I was confused. My HTML and EJS files were linked correctly, my routes were working, and I was sure I had declared agentData in my code. Yet every time I opened the /admin page, my app crashed.

What Actually Causing agentData is not defined?

The problem wasn’t with HTML or EJS at all — it was with how I structured my routes.

I realized I had two /admin routes:

  1. One in routes.js that did this:
res.render('adminhome', { agentData: data })
  1. Another one (in app.js or elsewhere) that just did:
res.render('adminhome')

When Express hit the second version, the one that didn’t pass agentData, my EJS tried to run:

if (agentData.length != 0) {

And of course, since agentData wasn’t passed, the template crashed with an “undefined” error.

Why Removing the /admin Route Gave Me “Cannot GET /admin”

When I deleted the “good” /admin route from routes.js, only the “bad” one (or none at all) was left.

That meant Express didn’t know how to fetch my data and render the view, so it gave me:

Cannot GET /admin

This was a good clue that my routes were overlapping and inconsistent.

Another Fragile Point EJS Checks

Even if I fixed the routes, I still had this in my EJS:

if (agentData.length != 0) {

If agentData was ever missing — maybe because of a database error — that line would throw an exception before the else could even run. The fix? Use safer checks like:

<% const list = Array.isArray(agentData) ? agentData : []; %>
<% if (list.length) { %>
  ...
<% } else { %>
  <tr><td colspan="5">No Data Found</td></tr>
<% } %>

That way, even if agentData wasn’t defined properly, my template wouldn’t explode.

The Fix

Here’s the approach I took:

  1. Have exactly one /admin route in routes.js that fetches from MySQL and renders the page with agentData.
  2. Mount my router once in app.js using app.use('/', routes).
  3. Defend in EJS by checking Array.isArray(agentData) before using .length.
  4. Make sure my “Edit” links match the route paths (/admin/edit/:id in both the EJS and router).

My Working Example

Here’s the fixed, minimal version with some extra features I added for practice.

app.js

const path = require('path');
const express = require('express');
const mysql = require('mysql2/promise');

const app = express();

// DB pool
const db = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'yourpass',
  database: 'yourdb',
});

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.urlencoded({ extended: false }));
app.set('db', db);

const routes = require('./routes');
app.use('/', routes);

app.use((req, res) => res.status(404).send('Not Found'));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`http://localhost:${PORT}`));

routes.js

const express = require('express');
const router = express.Router();

router.get('/', (req, res) => res.render('login'));

router.post('/login', (req, res) => {
  const { username, password } = req.body;
  if (username == 2 && password == 2) {
    return res.redirect('/admin');
  }
  res.status(401).json({ message: 'Auth fail' });
});

router.get('/admin', async (req, res, next) => {
  try {
    const db = req.app.get('db');
    const [rows] = await db.query('SELECT * FROM agentavia ORDER BY id DESC');
    res.render('adminhome', { title: 'Agent List', agentData: rows });
  } catch (err) {
    next(err);
  }
});

router.get('/admin/edit/:id', async (req, res, next) => {
  try {
    const db = req.app.get('db');
    const [rows] = await db.query('SELECT * FROM agentavia WHERE id = ?', [req.params.id]);
    if (!rows.length) return res.status(404).send('Agent not found');
    res.render('modifyAgent', { title: 'Edit Agent', editData: rows[0] });
  } catch (err) {
    next(err);
  }
});

router.post('/admin/edit/:id', async (req, res, next) => {
  try {
    const db = req.app.get('db');
    await db.query('UPDATE agentavia SET ? WHERE id = ?', [req.body, req.params.id]);
    res.redirect('/admin');
  } catch (err) {
    next(err);
  }
});

// Bonus practice features
router.get('/admin/create', (req, res) => res.render('modifyAgent', { title: 'Create Agent', editData: undefined }));
router.post('/admin/create', async (req, res, next) => {
  try {
    const db = req.app.get('db');
    await db.query('INSERT INTO agentavia SET ?', [req.body]);
    res.redirect('/admin');
  } catch (err) {
    next(err);
  }
});

router.get('/admin/delete/:id', async (req, res, next) => {
  try {
    const db = req.app.get('db');
    await db.query('DELETE FROM agentavia WHERE id = ?', [req.params.id]);
    res.redirect('/admin');
  } catch (err) {
    next(err);
  }
});

router.get('/admin/search', async (req, res, next) => {
  try {
    const { q = '' } = req.query;
    const db = req.app.get('db');
    const like = `%${q}%`;
    const [rows] = await db.query(
      `SELECT * FROM agentavia 
       WHERE agentName LIKE ? OR agentID LIKE ? OR agentStatus LIKE ?`,

[like, like, like]

); res.render(‘adminhome’, { title: `Search: ${q}`, agentData: rows }); } catch (err) { next(err); } }); module.exports = router;


views/adminhome.ejs (simplified with safe check)

<% const list = Array.isArray(agentData) ? agentData : []; %>
<% if (list.length) { %>
  <% list.forEach((row, idx) => { %>
    <tr>
      <td><%= idx + 1 %></td>
      <td><%= row.agentName %></td>
      <td><%= row.agentID %></td>
      <td><%= row.agentStatus %></td>
      <td>
        <a href="/admin/edit/<%= row.id %>">Edit</a> |
        <a href="/admin/delete/<%= row.id %>">Delete</a>
      </td>
    </tr>
  <% }) %>
<% } else { %>
  <tr><td colspan="5">No Data Found</td></tr>
<% } %>

Final Thought

In the end, fixing the agentData is not defined error came down to keeping my routes consistent, always passing the right data into my views, and writing defensive EJS checks. Once I streamlined my /admin route and cleaned up the render logic, everything worked smoothly. It was a good reminder that most “undefined” errors aren’t about HTML being wrong they’re about how data flows between the backend and the templates.

Related blog posts