HTMLAnchorElement.download: Configurable Descriptor

Sep 16, 2026·19 min read

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.download is an enumerable, configurable accessor property whose native getter and setter reflect an anchor element’s download content attribute.

The Exact download Property Descriptor

JavaScript propertyHTML storageanchor.downloadGETTERSETTERdownloadattributeno valueThe descriptor stores functions, not the filename
The inherited accessor routes reads and assignments to the anchor’s content attribute.

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:

  • get is the native function used when the property is read.
  • set is the native function used when the property is assigned.
  • enumerable: true makes the property enumerable on the object that owns it.
  • configurable: true permits 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

one <a> elementno own downloadown-only checkstops hereundefinedproperty accesskeeps lookingprototype getterSame element, two different questions
Two questions about the same anchor follow different search routes.

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

Web IDL declaration[Reflect] attributeDOMString downloadWHERE?prototypenot on eachanchorSHAPEgetter+ setterno valueFLAGSenumerableconfigurableboth trueOne declaration controls all three
The Web IDL declaration determines the property’s location, shape, and flags.

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 detailJavaScript descriptor result
attributeThe property has a getter
No readonly keywordThe property also has a setter
Regular interface attributeThe property is installed on the interface prototype
Web IDL attribute ruleenumerable is true
Not declared unforgeableconfigurable 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 value and writable.
  • An accessor descriptor uses get and set.

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

BEFOREAFTERnative accessorget + setno value fieldconfigurableredefinedata propertyfixed valuewritable trueconfigurable trueKeep it configurableso native access can return
Redefinition can replace the native accessor with a completely different descriptor shape.

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.

existinganchorcreated lateranchorsharedprototypeonereplacementanotheranchor
Many anchors in one realm consult the same patched prototype.

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.

SAFE ROUTEwrapperget()saved nativeget.call(this)anchorattributeRECURSIVE ROUTEwrapperthis.downloadrecursionnative notreached
Calling the saved accessor reaches native behavior; reading the property again causes recursion.

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.

Window ApatchedprototypeA1A2anchors from AWindow Biframe realmown nativeprototypeB1B2anchors from BNavigation creates a new realm
A prototype patch stays inside the window realm that owns that 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.

Frequently asked questions

Is HTMLAnchorElement.prototype.download configurable?
Yes. Its property descriptor has configurable set to true because download is a regular Web IDL attribute and is not declared unforgeable. JavaScript can delete or redefine the property on HTMLAnchorElement.prototype.
Why does Object.getOwnPropertyDescriptor(anchor, "download") return undefined?
Object.getOwnPropertyDescriptor() checks only the object passed to it. A normal anchor inherits download from HTMLAnchorElement.prototype, so the anchor itself has no own download property.
Why is writable missing from the download property descriptor?
download is an accessor property with get and set functions, not a data property with value and writable fields. Its setter makes assignment possible, but that does not add a writable field to the descriptor.
How do you restore the native download accessor after overriding it?
Save the complete original descriptor before calling Object.defineProperty(). In a finally block, restore it only if the wrapper is still installed; if another patch replaced it, leave that later descriptor intact and report the conflict without replacing an exception from the test or patched code.