skip to content

What is a `static { ... }` initialization block in a JavaScript class body — when does it run, and what is `this` inside it?

level: middleimportance: should knowfreq 34%

answer

  1. runs once at class-definition time
  2. interleaved with static fields, source order
  3. this is the class, super is the parent
  4. statements allowed, so try/catch and loops
  5. can touch the class's #private names

basics

~20 s

A static initialization block is a chunk of statements that runs once while the class is being defined, in source order with the static field initializers. Inside it, this is the class constructor, and it can read the class's private names.

solid answer

~50 s

Static blocks arrived in ES2022 to give class-level setup somewhere to live. A `static { ... }` body is evaluated exactly once, during class definition, interleaved in source order with the static field initializers — so a block sees fields declared above it and not those below. `this` is the class itself, and `super.x` reaches the parent class's static properties. Because it is a statement block rather than a single expression, it can use `try/catch`, loops, and temporaries — which is what a field initializer cannot do. The other reason it exists is privacy: a static block can touch the class's `#private` names, so it is the sanctioned way to hand a trusted helper outside the class an accessor for private state. Restrictions worth knowing: no `return`, no `arguments`, and `await` and `yield` are not usable there.

code

javascript · 11 lines
javascript
class Config {
  static a = 1;
  static { console.log('block 1 sees:', this.a, this.b); } // 1 undefined
  static b = 2;
  static table = new Map();
  static {
    for (const [k, v] of [['x', 10], ['y', 20]]) this.table.set(k, v);
    console.log('block 2 sees:', this.a, this.b, this === Config);
  }
}
console.log(Config.table.get('y')); // 20

go deeper

for a junior

Recognise the static { ... } syntax and be able to say it runs once when the class is defined, with this meaning the class. Knowing it exists is enough at this level.

for a middle

Explain the interleaving with static field initializers in source order, why a statement body beats a single-expression initializer, and that the block can reach the class's private names.

for a senior

Judge what belongs there: definition-time validation and table building yes, network or timer side effects no, because the block fires the moment the module is evaluated. Know that a throw kills the class binding entirely.

for a principal

Weigh eager class-definition-time setup against lazy initialization for start-up cost and testability across a large codebase, and set a house rule for when configuration failure should abort module evaluation instead of surfacing at first use.

## The gap it fills Before ES2022 there were two ways to do class-level setup, and both were awkward. You could cram logic into a static field initializer, which must be a single expression — so a loop or a `try/catch` meant smuggling in an immediately-invoked function. Or you could run statements *after* the class declaration, which leaves the class briefly half-configured and puts its setup somewhere a reader will not look. A static initialization block puts that code inside the class body where it belongs. ```js class Registry { static byCode = new Map(); static { for (const [code, label] of [[1, 'ok'], [2, 'retry'], [3, 'fail']]) { this.byCode.set(code, label); } } } Registry.byCode.get(2); // "retry" ``` ## When it runs Once, during evaluation of the class definition — the same moment the static fields are initialized, and before any other code can observe the finished class. Blocks and static fields share one ordering: **source order, top to bottom**. A block can therefore read a field declared above it and will see `undefined` for one declared below. ```js class Ordering { static a = 1; static { console.log(this.a, this.b); } // 1 undefined static b = 2; static { console.log(this.a, this.b); } // 1 2 } ``` A class can contain any number of blocks, and they all run in that same interleaved order. Because this is class-definition time, a throw inside a block propagates out of the `class` statement itself and the class binding is never created — useful for fail-fast validation of configuration the class depends on. ## What `this` is `this` is the class constructor being defined — the same value a static method sees when called on the class. There is no instance in scope, because none exists yet. `super` is also available and refers to the parent *class*, so `super.someStatic` reads the parent's static property: ```js class Parent { static defaults = { retries: 3 }; } class Child extends Parent { static config; static { this.config = { ...super.defaults, retries: 5 }; } } Child.config; // { retries: 5 } ``` The class binding is also in scope by name inside the block, so `Registry.byCode` works as well as `this.byCode`. Prefer `this` when a subclass should get its own value, and the explicit name when it must not vary. ## Access to private names A static block is inside the class body, so it can use the class's `#private` names — including private *instance* names, in the sense that it can capture a function that will later read them. That makes it the sanctioned mechanism for a controlled leak, sometimes called the friend pattern: ```js let readSecret; class Vault { #secret = 42; static { readSecret = (v) => v.#secret; } } readSecret(new Vault()); // 42 ``` Without the block you would have to define such an accessor in a constructor or a method, which runs per instance instead of once. ## The restrictions A static block is not a function body, and the grammar reflects that. `return` is a syntax error. `arguments` is not available. `await` and `yield` cannot be used as expressions there, so no top-level await inside a block. `var` declarations and function declarations inside it are scoped to the block, not to the class or the module. The block does have its own variable scope, so temporaries you declare there do not leak. ## When to reach for one Good uses: building a lookup table or frozen constant that needs more than one expression; validating environment or configuration at definition time so a misconfigured class fails immediately rather than on first call; wiring a subclass into a registry the parent owns; setting up the friend accessor above. Weak uses: anything with side effects beyond the class — network calls, timers, writes to shared modules — because the block runs the instant the module containing the class is evaluated, whether or not anyone ends up using the class. Setup that is expensive or fallible is often better done lazily behind a static getter.

  • Why can't you just do this work in a static field initializer?
    A field initializer must be a single expression, so anything needing a loop, a try/catch, or a temporary variable has to be wrapped in an immediately-invoked function just to get a statement body. A static block is that statement body, natively, with `this` already bound to the class.
  • What happens if a static block throws?
    The exception propagates out of the class definition itself. Evaluation stops, the class binding is never initialized, and any later reference to it hits the temporal dead zone or a ReferenceError. That makes a block a reasonable place for fail-fast validation of something the class cannot work without.
  • Can you await inside a static initialization block?
    No. `await` is not usable as an expression there, so asynchronous setup has to live elsewhere — typically a static async initializer the caller awaits, or a lazily-created promise held in a static field. A block is strictly synchronous, definition-time code.

saying these in an interview costs you the question

  • Says the block runs once per instance
  • Thinks this refers to an instance inside the block
  • Claims blocks always run after all static fields
  • Expects await or return to work inside a block
  • Believes a block is the only way to set static fields

context