Classic ASP / JScript – ES6 Built-ins That Don't Survive the Build
KYD apps (SPM, EMP, LPA, CFT, lite) build their Node-style API source into a
single Classic ASP file with the goldfish bundler. Babel transpiles
syntax (arrow functions, const/let, destructuring, spread, classes,
async/await), but the ASP runtime is JScript (an old ES3-era engine), so
any ES6 built-in object or method that Babel can't down-compile must exist at
runtime. The goldfish polyfill server (/v2/polyfill.min.js) provides a
limited set of these. Some it cannot provide at all.
The code runs fine under npm run dev (real Node) and only breaks once it's
running as ASP — which is why these bugs slip through.
❌ NOT available — never use in API source
These have no JScript equivalent and the polyfill server cannot shim them
(a faithful Set/Map needs the iterator protocol / Symbol.iterator, which
JScript lacks). Replace them with plain objects/arrays.
| Construct | Why it breaks | Use instead |
|---|---|---|
new Set() |
No Set constructor in JScript |
Plain object {} as a lookup map |
new Map() |
No Map constructor in JScript |
Plain object {} keyed by string |
Array.from(x) |
Not defined | [].slice.call(x) or build the array explicitly |
[...Array(n)] |
Spread of a sparse array — the polyfilled .map() skips holes (i in arr), yielding empty results |
An explicit array literal [1, 2, 3, …, n] |
Object.fromEntries(x) |
Not polyfilled — throws "Object doesn't support this property or method" | A plain loop that builds the object (see below) |
⚠️
Object.fromEntriesis NOT available even thoughObject.entriesIS. The polyfill server shipskeys/entries/assignbut notfromEntries, so the commonObject.fromEntries(Object.entries(x).filter(…))idiom blows up only once it runs as ASP.
Replacing Set with a plain object
A Set is just membership + uniqueness. A plain object does both.
// ❌ Breaks in ASP
const seen = new Set()
rows.filter((row) => {
if (seen.has(row.id)) return false
seen.add(row.id)
return true
})
// ✅ Works everywhere
const seen = {}
rows.filter((row) => {
if (seen[row.id] === true) return false
seen[row.id] = true
return true
})
Use === true, not just truthiness. A bare if (seen[key]) can false-match
inherited prototype members — seen['toString'] returns a function (truthy)
even though you never added it. Storing true and testing === true sidesteps
that, because inherited members are never === true.
When you need the Set's contents back as a list:
// ❌ const roles = [...roleSet].sort()
// ✅
const roles = Object.keys(roleObj).sort() // string keys
const years = Object.keys(yearObj).map(Number).sort((a, b) => b - a) // numeric
Replacing Map with a plain object
// ❌ const index = new Map(); index.get(key); index.set(key, entry)
// ✅
const index = {}
let entry = index[key]
if (!entry) {
entry = { /* … */ }
index[key] = entry
}
Safe as long as the key can't collide with an Object.prototype member. Keys
that are composites like [year, tid, status].join('|') are always safe — they
contain the | separator, so they can never equal toString, constructor,
__proto__, etc. If a key could be arbitrary user text, guard with
Object.prototype.hasOwnProperty.call(index, key).
Replacing Object.fromEntries
// ❌ Breaks in ASP — "Object doesn't support this property or method"
const picked = Object.fromEntries(
Object.entries(settings).filter(([key]) => allowed.includes(key))
)
// ✅ Works everywhere
const picked = {}
for (const key of allowed) {
if (Object.prototype.hasOwnProperty.call(settings, key)) {
picked[key] = settings[key]
}
}
❌ Static namespace methods — NONE are polyfilled
autopolyfiller only detects prototype methods, so static Number.* / Math.*
(ES2015) / Object.values are never requested from the polyfill server and throw
"Object doesn't support this property or method" at runtime. These run fine under
Node, so they slip through until ASP.
| Construct | Use instead |
|---|---|
Number.isNaN(x) |
global isNaN(x) |
Number.isFinite(x) |
global isFinite(x) |
Number.parseInt / Number.parseFloat |
global parseInt / parseFloat |
Number.isInteger(x) |
typeof x === 'number' && isFinite(x) && Math.floor(x) === x |
Math.trunc(x) |
x < 0 ? Math.ceil(x) : Math.floor(x) |
Math.sign(x) |
x > 0 ? 1 : x < 0 ? -1 : x |
other ES2015 Math.* (cbrt,hypot,log2,log10,clz32,fround,imul,expm1,log1p,sinh…) |
implement manually |
Object.values(o) |
Object.keys(o).map(function (k) { return o[k] }) |
⚠️
Number.isNaN/isFinitediffer from the globals only in that the globals coerce their argument first. When the value is already numeric (the usual case:Number(x),parseInt,.getTime(),.getFullYear()) the swap is exact.
❌ Prototype methods that are NOT polyfilled
autopolyfiller (circa 2017) fetches only a subset of prototype methods. Anything
it doesn't request is silently skipped and throws "Object doesn't support this
property or method" at runtime — even though it's a prototype method and even
when it's ES2015. Don't assume "ES2015 = safe." Verify against a shipped bundle
if unsure (grep -c "prototype.theMethod" Default.asp).
- Polyfilled (safe):
.find/.findIndex/.includes/.repeat/.entries/.values(.includesis also Babel-rewritten toindexOf). - NOT polyfilled (avoid):
| Construct | Era | Use instead |
|---|---|---|
str.startsWith(x) |
ES2015 | s.indexOf(x) === 0 or /^x/.test(s) |
str.endsWith(x) |
ES2015 | /x$/.test(s) or s.slice(-x.length) === x |
str.padStart(n, '0') |
ES2017 | ("0".repeat(n) + str).slice(-n) (or ("0" + n).slice(-2) for 2-digit) |
str.padEnd(n, c) |
ES2017 | concatenate manually |
str.trimStart() / trimEnd() |
ES2019 | .replace(/^\s+/, "") / .replace(/\s+$/, "") |
arr.flat() |
ES2019 | flatten with reduce/concat |
arr.flatMap(fn) |
ES2019 | .map(fn).reduce(function (a, b) { return a.concat(b) }, []) |
str.replaceAll(a, b) |
ES2021 | .replace(/a/g, b) or .split(a).join(b) |
str.matchAll(re) |
ES2021 | global regex with .exec() in a loop |
arr.findLast() / findLastIndex() |
ES2023 | iterate in reverse |
.repeat()and.includes()ARE polyfilled, but the closely-related.startsWith/.endsWithare not — a classic foot-gun, since all three are ES2015.
✅ Available — the polyfill server DOES provide these
These are safe to use in API source; KYD codebases already rely on them in shipped ASP builds:
Object.keys,Object.entries,Object.assign(but notObject.fromEntries/Object.values— see the tables above)Array.prototype.find/findIndex/includes/repeat,String.prototype.includes(Babel also rewrites.includes()toindexOf() !== -1in many cases) — but notstartsWith/endsWith/padStart/padEnd, see the table aboveObject.assignfalls back to a manual_extendshelper when the runtime lacks it, so it's safe regardless.
Because Object.keys is available, the idiomatic Set replacement
Object.keys(obj).sort() is fully supported.
How to catch these before deploying
node --check file.js only validates syntax — it will happily pass code
full of new Set(). To find runtime built-ins, grep the API source:
grep -rn "new Set(\|new Map(\|Array\.from\|\[\.\.\.Array\|Object\.fromEntries" api/src
The application's own source is usually the only offender. The lite framework itself is clean — these patterns creep in when API utilities/routes are written "Node-first" and never exercised as an ASP build.