← All context files

classic-asp-jscript/runtime-gotchas.md

Classic ASP / JScript – Runtime Gotchas

Beyond missing ES6 built-ins (see es6-builtins.md), the JScript/ASP runtime behaves differently from Node in ways Babel can't paper over. These are the recurring ones in KYD apps (lite v6 framework: SPM, EMP, LPA, CFT).

'Reflect' is undefined — a class extending a native (usually Error)

Symptom: the bundle throws 'Reflect' is undefined at runtime in ASP.

Cause: Babel compiles class X extends Error (or any native built-in) with _wrapNativeSuper(Error)_isNativeReflectConstruct()Reflect.construct. JScript has no Reflect, so it crashes the moment that path executes.

Most common trigger: two route files registered for the same path (e.g. a GET file and a POST file). The wrong-method request reaches validatedMethod, which throws new ClientError(msg, 405) — and ClientError extends Error is exactly the construct that needs Reflect.

Fix: never split methods for one path across two files. Use a single route with methods: ['GET', 'POST'] and branch inside preprocess on req.method. Note the route pipeline still calls validatedMethod even when you guard in preprocess, so the single-file form is mandatory, not just tidier.

Response.End() / send() halts execution — but only in ASP

In ASP, Response.End() terminates the script immediately. The lite send() / end() helpers call it, so in an ASP build everything after a send() is dead code. A JScript try/catch does NOT catch Response.End.

In Node, send() does not halt — execution continues past it. So a conditional send() that you expect to short-circuit must be followed by an explicit return, or the Node and ASP builds diverge:

if (!user) {
  send(res, 401, { error: 'Unauthorized' })
  return   // ← required, or Node keeps running the handler
}

IIS routing — no URL Rewrite, no PATH_INFO

IIS Classic ASP rejects index.asp/path/info URLs by default (allowPathInfo is off on the ASP handler), and dev servers often have no URL Rewrite module.

Route ordering & required fields

Query parsing (identical across Node dev, ASP dev, ASP prod)

The compiled qs + reviveQuery code is the same everywhere, so these hold in all three environments:

Fetch-all + slice-in-JS times out in ASP (but flies in Node)

Symptom: a list endpoint works instantly under npm run dev (Node) but times out (~60s) once deployed as Classic ASP.

Cause: a query that pulls the whole table/view and paginates in JavaScript — select(db, view, { itemsPerPage: 10000, paginate: false }) then .slice() / .filter() / .length in JS. In Node the dev DB is tiny and the rows marshal for free. In ASP, lite reads results through an ADO recordset, and every row and every field is a COM round-trip — so thousands of rows × N columns is tens of thousands of COM calls. Large prod views (e.g. a CFT_Users view over all Defence accounts) blow the request timeout. This is a data-volume cliff: it won't show in dev and gets worse as prod data grows.

Fix: let SQL do the paging and counting. select() with page + itemsPerPage and paginate: true (the default) emits a ROW_NUMBER() window that returns only the requested page, plus a separate getCount() for meta.totalItems. Only one page of rows is ever marshalled:

// ❌ times out in ASP on a large view
const all = select(db, 'CFT_Users', { columns, filter, itemsPerPage: 10000, paginate: false })
const data = all.data.filter(r => r.id && r.name).map(normalize).slice(offset, offset + perPage)

// ✅ SQL does the paging + count
const result = select(db, 'CFT_Users', {
  columns, filter, sort,
  page, itemsPerPage,        // paginate defaults to true
})
const data = result.data.map(normalize)
const totalItems = result.meta.totalItems

Push any JS-side row exclusions into the SQL filter (e.g. { key: 'name', isNotNull: true }) so the COUNT and the page agree. Bounded lookups (one user's roles, reference data of a few dozen rows) are fine to fetch whole — this only bites unbounded result sets. For reporting endpoints that genuinely need every row to aggregate, push the aggregation into SQL with group (GROUP BY) rather than summing thousands of marshalled rows in JS.

DB connection

The DB connection comes from Application.contents('SQLConn') (set in global.asa / IIS Application state), not from a database param passed to select(). Routes that target a non-default DB pass it explicitly, e.g. database: process.env.CFT_LEGACY_DB_NAME.