swc-project/swc · error · TypeError

"${name}" is read-only

Error message

"${name}" is read-only

What it means

SWC's ES2022 class-properties transform lowers private accessors; when the transform sees an assignment to a private accessor that declares only a getter, it emits `_read_only_error("#x")` (crates/swc_ecma_compat_es2022/src/class_properties/private_field.rs:367). At runtime the helper throws `TypeError: "#x" is read-only`, matching the TypeError engines produce for writing a get-only private accessor.

Source

Thrown at crates/swc_ecma_transforms_base/src/helpers/generated/_read_only_error.rs:11

// This file is generated by `cargo codegen helpers`. DO NOT MODIFY.

use super::{HelperDef, HelperName};

pub const DEF: HelperDef = HelperDef {
    name: HelperName::read_only_error,
    local: "_read_only_error",
    import_path: "@swc/helpers/_/_read_only_error",
    #[cfg(feature = "inline-helpers")]
    source: r#"function _read_only_error(name) {
    throw new TypeError("\"" + name + "\" is read-only");
}
"#,
    #[cfg(feature = "inline-helpers")]
    deps: super::HelperBitmap::from_bits(0x00000000000008000000000000000000),
};

#[cfg(feature = "inline-helpers")]
pub fn stmts() -> &'static [swc_ecma_ast::Stmt] {
    static STMTS: once_cell::sync::Lazy<Vec<swc_ecma_ast::Stmt>> =
        once_cell::sync::Lazy::new(|| super::super::parse(DEF.source, DEF.import_path));
    &STMTS
}

View on GitHub (pinned to 5176682b65)

Solutions

  1. Add a matching private setter: `set #x(v) { this._x = v; }`.
  2. Store state in a plain private field (`#x`) and expose a getter, mutating the field directly instead of the accessor.
  3. Remove the assignment; the design is read-only, so move the mutation inside the class where the backing field lives.

Example fix

// before
class Counter {
  get #value() { return this._v; }
  increment() { this.#value = this.#value + 1; } // TypeError: "#value" is read-only
}

// after
class Counter {
  #value = 0;
  get value() { return this.#value; }
  increment() { this.#value += 1; }
}
Defensive patterns

Strategy: try-catch

Try / catch

// Private accessors cannot be reflectively inspected, so guard the mutation site.
try {
  this.#value = next;
} catch (err) {
  if (err instanceof TypeError && /is read-only/.test(err.message)) {
    // accessor has no setter — route through the mutation API instead
    this.writeInternal(next);
  } else throw err;
}

Prevention

When it happens

Trigger: `class A { get #x() { return this._x; } write(v) { this.#x = v; } }` — any assignment (`this.#x = ...`, compound assigns, `++`) to a get-only private accessor in code downleveled by the class-properties pass.

Common situations: Declaring intent as read-only via a getter-only private accessor and later adding a mutation path that forgets to add the setter; refactoring a public getter to `#private` while call sites still write; code generators emitting writes to computed private accessors.

Related errors


AI-assisted analysis of swc-project/swc@5176682b65 (2026-08-17). Data as JSON: /api/errors/4c5483e07ea8368b. Report an issue: GitHub.