HTMLAnchorElement.download: Configurable Descriptor
You inspect HTMLAnchorElement.prototype.download and find configurable: true, but no value or writable field. That shape comes from Web IDL: download is a browser-defined accessor on the prototype, complete with native getter and setter functions.
HTMLAnchorElement.prototype.downloadis an enumerable, configurable accessor property whose native getter and setter reflect an anchor element’sdownloadcontent attribute.
The Exact download Property Descriptor
Run the inspection in a browser Window realm, where HTMLAnchorElement exists:
if (typeof HTMLAnchorElement === "undefined") {
throw new Error(
"Run this inspection in a browser Window realm"
);
}
const descriptor = Object.getOwnPropertyDescriptor(
HTMLAnchorElement.prototype,
"download"
);
if (
typeof descriptor.get !== "function" ||
typeof descriptor.set !== "function" ||
descriptor.enumerable !== true ||
descriptor.configurable !== true
) {
throw new TypeError(
"Expected an enumerable, configurable accessor"
);
}
console.log("{");
console.log(" get: [native getter function],");
console.log(" set: [native setter function],");
console.log(" enumerable: true,");
console.log(" configurable: true");
console.log("}");
Console rendering varies among browsers, but the accessor functions have the standardized names get download and set download, so the useful result is this schematic shape:
{
get: [native getter function],
set: [native setter function],
enumerable: true,
configurable: true
}
This is an accessor descriptor. Reading anchor.download calls get, and assigning to anchor.download calls set. There is no stored value field in the descriptor itself.
The four fields mean:
getis the native function used when the property is read.setis the native function used when the property is assigned.enumerable: truemakes the property enumerable on the object that owns it.configurable: truepermits deletion and redefinition of that prototype property.
The download property reflects the anchor’s download content attribute as a string. The attribute indicates that the hyperlink is intended for downloading a resource, and a nonempty value recommends a default filename.
Start with the visible reflection:
const anchor = document.createElement("a");
anchor.download = "quarterly-report.csv";
console.log(anchor.getAttribute("download"));
console.log(anchor.download);
quarterly-report.csv
quarterly-report.csv
The setter updates the content attribute, and the getter reads its reflected value. Attributes and properties covers that relationship across more DOM APIs.
Why the Descriptor Lives on the Prototype
An own property belongs directly to the object being inspected. An inherited property belongs to an object farther up its prototype chain.
A normal anchor element inherits download from HTMLAnchorElement.prototype. Object.getOwnPropertyDescriptor() retrieves only an own property descriptor, so it does not walk that chain for you.
This standalone browser example checks both locations:
const anchor = document.createElement("a");
console.log(
Object.getOwnPropertyDescriptor(anchor, "download")
);
console.log(
Object.getOwnPropertyDescriptor(
HTMLAnchorElement.prototype,
"download"
) !== undefined
);
console.log(
Object.getPrototypeOf(anchor) ===
HTMLAnchorElement.prototype
);
undefined
true
true
The first result does not mean anchor.download is unavailable. It means the property is not defined directly on anchor.
Property access follows the prototype chain. JavaScript checks the anchor first, finds no own download property, reaches HTMLAnchorElement.prototype, and invokes the inherited getter with the anchor as its receiver. That receiver is why the getter reads the attribute from the particular element you used.
This distinction matters whenever you inspect browser objects. Object.getOwnPropertyDescriptor(anchor, "download") asks, “Does this anchor own the property?” The prototype call asks, “Where is the browser API member defined?”
You can locate an inherited descriptor with a small search:
function findPropertyOwner(object, property) {
let current = object;
while (current !== null) {
if (Object.hasOwn(current, property)) {
return current;
}
current = Object.getPrototypeOf(current);
}
return undefined;
}
const anchor = document.createElement("a");
console.log(
findPropertyOwner(anchor, "download") ===
HTMLAnchorElement.prototype
);
true
Prototypal inheritance explains the lookup itself, while native prototypes covers prototypes supplied by the JavaScript environment and browser.
An assignment can still create an own property when another object inherits an ordinary writable data property. That is not what happens here. The inherited download member has a setter, so the assignment invokes that setter and reflects the attribute instead of creating a separate own data property.
How Web IDL Produces the Descriptor
The HTML Standard declaration for HTMLAnchorElement includes this member:
[CEReactions, Reflect] attribute DOMString download;
HTML defines what the member represents. Web IDL defines how that interface member appears to JavaScript.
Under Web IDL’s rules for attributes, a regular attribute is exposed on its interface prototype object unless the declaration puts it somewhere else through specific rules. download is a regular attribute, so its JavaScript property belongs to HTMLAnchorElement.prototype.
The Web IDL attribute algorithm then creates an accessor descriptor. That decision accounts for the descriptor fields you see:
| Web IDL declaration detail | JavaScript descriptor result |
|---|---|
attribute | The property has a getter |
No readonly keyword | The property also has a setter |
| Regular interface attribute | The property is installed on the interface prototype |
| Web IDL attribute rule | enumerable is true |
| Not declared unforgeable | configurable is true |
The getter and setter are browser-provided functions. The [Reflect] declaration connects their work to the corresponding content attribute, which is why assigning "quarterly-report.csv" through the property changes getAttribute("download") too.
This also explains the missing writable field. JavaScript has two descriptor shapes:
- A data descriptor uses
valueandwritable. - An accessor descriptor uses
getandset.
A descriptor cannot use both shapes at once. Assignment works because download has a setter, not because it has writable: true.
configurable and writable answer different questions. configurable controls whether the property definition can be removed or structurally changed. writable controls assignment to the value held by a data property.
The browser environment and specifications explains how platform specifications become APIs available to JavaScript.
What configurable: true Actually Permits
A configurable property can be deleted and defined again. It can also be redefined with a different descriptor shape, including a change from an accessor property to a data property.
That power applies to the property definition on HTMLAnchorElement.prototype. It does not mean the native getter stores a writable value there, and it does not make prototype modification harmless.
This standalone experiment deletes the native accessor, replaces it with a writable data property, inspects the new shape, and restores the complete native descriptor:
if (typeof HTMLAnchorElement === "undefined") {
throw new Error(
"Run this experiment in a browser Window realm"
);
}
const prototype = HTMLAnchorElement.prototype;
const original = Object.getOwnPropertyDescriptor(
prototype,
"download"
);
try {
const deleted = delete prototype.download;
Object.defineProperty(prototype, "download", {
value: "forced-name.txt",
writable: true,
enumerable: original.enumerable,
configurable: true,
});
const replacement = Object.getOwnPropertyDescriptor(
prototype,
"download"
);
console.log(deleted);
console.log(
replacement.value,
replacement.writable,
"get" in replacement
);
} finally {
Object.defineProperty(
prototype,
"download",
original
);
}
true
forced-name.txt true false
The deletion succeeds because the original descriptor is configurable. Object.defineProperty() then installs a data property, so the replacement has value and writable instead of get and set.
The replacement deliberately keeps configurable: true. If you redefine the property as non-configurable, the cleanup code cannot freely put the native accessor back. A temporary patch should preserve a path to restoration.
Object.defineProperty() also supplies false for omitted Boolean descriptor fields. This shorter replacement would therefore be dangerous:
Object.defineProperty(
HTMLAnchorElement.prototype,
"download",
{
value: "forced-name.txt",
}
);
It creates a non-writable, non-enumerable, non-configurable data property. The original descriptor may have been configurable, but that does not rescue the new definition after you explicitly replace it.
Done? Not quite.
Changing the shared prototype affects property lookup for every matching anchor in that Window realm. Existing elements and elements created later reach the same replacement unless they have an own property that stops the lookup first.
Use a configurable prototype override as a bounded testing or instrumentation technique, not as ordinary application state. Store application behavior in your own functions and objects when you control the call site.
Override the Accessor Without Breaking It
Replacing the accessor with a fixed value throws away its reflection behavior. A wrapper keeps that behavior by saving the complete descriptor, calling the original accessors with the current receiver, and restoring the saved descriptor in a finally block.
Here is the canonical synchronous pattern:
function withDownloadAudit(realm, task) {
const prototype =
realm.HTMLAnchorElement.prototype;
const original = Object.getOwnPropertyDescriptor(
prototype,
"download"
);
if (
!original ||
typeof original.get !== "function" ||
typeof original.set !== "function" ||
original.configurable !== true
) {
throw new TypeError(
"Expected a configurable download accessor"
);
}
const events = [];
const wrapped = {
...original,
get() {
const value = original.get.call(this);
events.push({
type: "get",
element: this,
value,
});
return value;
},
set(value) {
original.set.call(this, value);
events.push({
type: "set",
element: this,
value,
});
},
};
Object.defineProperty(
prototype,
"download",
wrapped
);
try {
return task(events);
} finally {
const current = Object.getOwnPropertyDescriptor(
prototype,
"download"
);
const stillInstalled =
current &&
current.get === wrapped.get &&
current.set === wrapped.set &&
current.enumerable === wrapped.enumerable &&
current.configurable === wrapped.configurable;
if (!stillInstalled) {
throw new Error(
"The download descriptor changed during the audit"
);
}
Object.defineProperty(
prototype,
"download",
original
);
}
}
const result = withDownloadAudit(
window,
(events) => {
const anchor =
document.createElement("a");
anchor.download = "report.csv";
const filename = anchor.download;
return {
filename,
operations: events
.map((event) => event.type)
.join(", "),
};
}
);
console.log(result.filename);
console.log(result.operations);
report.csv
set, get
original.get.call(this) and original.set.call(this, value) preserve the receiver. Inside those native accessors, this remains the anchor that was read or assigned.
Reading this.download from inside the wrapper would start property lookup again, find the wrapper again, and recurse. The saved functions provide the route back to the native behavior.
The spread copies the original enumerable and configurable fields along with its accessors. The wrapper replaces only get and set, and, if that wrapper is still installed, the final Object.defineProperty() restores the descriptor that existed before the patch.
The identity check catches another patch made while the audit is active. Restoring blindly would erase that later change, so this implementation reports the conflict, leaves the later descriptor intact, and does not throw a new error that could replace an exception from the task.
Prototype changes are realm-specific. An iframe has its own HTMLAnchorElement constructor and its own HTMLAnchorElement.prototype; passing window patches the current realm only. To inspect a same-origin iframe, pass its contentWindow after its document is available, and have the callback create or use elements from that iframe’s document. A cross-origin frame must run the patch within its own origin because the parent cannot access its constructor or document. Navigation replaces the iframe’s realm and therefore its patched prototype.
Tests should install the wrapper for the smallest possible scope and remove it before the next test starts. Libraries, browser instrumentation, or another test can patch the same descriptor, and configurable: true provides no coordination between them.
For a broader treatment of browser APIs, DOM behavior, and the prototype chain, The Browser Platform collects the corresponding lessons for offline reading.