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
- Add a matching private setter: `set #x(v) { this._x = v; }`.
- Store state in a plain private field (`#x`) and expose a getter, mutating the field directly instead of the accessor.
- 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
- Declare accessor pairs together (`get #x()` with `set #x()`) or skip accessors entirely and use a plain private field `#x`.
- Lint against assignments to getter-only members (TS emits error TS2540 for the native case; keep the same discipline for `#private`).
- When a value is read-only by design, expose mutation only through named methods so no call site ever assigns.
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
- "${name}" is write-only
- Decorating class property failed. Please ensure that proposa
- Non-computed member expression with property other than iden
- getter/setter property be compiled as CompiledProp::Accessor
- module string names unimplemented
AI-assisted analysis of swc-project/swc@5176682b65 (2026-08-17).
Data as JSON: /api/errors/4c5483e07ea8368b.
Report an issue: GitHub.