Skip to content

Initialization Order

Heliguy edited this page May 19, 2026 · 1 revision

The initialization order of a subclass is what determines when different items will be available, when things will run, and when and where undefined may show up. GObjectify does everything in its power to avoid requiring the memorization of the init order, but it is useful to understand nonetheless.

The GJS Construction Lifecycle

When you write new MyWidget({ title: "Hello" }), a specific sequence of events happens before your instance is returned. Understanding this sequence is key to understanding why things are or aren't available at different points.

  1. GObject allocates the instance

GObject allocates memory for the instance and sets up the GType system's internal bookkeeping. Nothing visible from JS has happened yet.

  1. _init runs

GJS's internal _init function runs. This is NOT your constructor. _init is a GJS hook that runs as part of GObject's construction machinery, and GObjectify hooks into _init to apply any SimpleActions.

After _init completes, template children are bound. _init runs inside the parent class's construction (super(), if you've overridden the constructor).

  1. vfunc_constructed runs (if overridden)

If you've overridden the vfunc_constructed method on your subclass, it is then run here. Like with _init, vfunc_constructed also runs inside the parent class's construction. This means that vfunc_constructed does not have access to any fields on your subclass, they will all be undefined if accessed in vfunc_constructed.

vfunc_constructed does have access to most properties. "const", "readonly", and "readwrite" properties are visible, and their values are either their default value, or a value supplied via super()/new. "computed" properties are not fully available, and shouldn't be accessed at this point.

  1. JS field initializers run

After the parent class is done initializing (after super() returns if you've overridden the constructor), the JS engine runs all the class field initializers. These are things like count = 10, user = new User(), #keyfile = new GLib.Keyfile(). This is the reason why vfunc_constructed, and any other hooks that occur in super cannot see your class's fields: they haven't been initialized yet.

This is why "computed" properties are not flagged as CONSTRUCT and don't run their getters/setters before super() finishes. If they were and were able to, GObject would call your getter and setter during super(), before your backing fields exist at all.

  1. The rest of your constructor body runs

If you've overridden the constructor, this is when the rest of your constructor body runs. After super() completes, and after the fields are all initialized, only then can the JS engine move on to the rest of your constructor.

This is first point where everything is guaranteed to be available:

  • Template children
  • All properties
  • All fields (including JS private #fields)
  1. @WatchProp's initial call and @PostInit run (next idle)

On the next idle iteration of the GLib main loop, methods marked with @WatchProp and @PostInit will run. These run asynchronously on idle. Everything is available, and the widget is already visible and usable by this point.

UI Callbacks

One complication is that any UI callbacks may fire during the super() portion of initialization. Take a look at the following:

@GClass({ template: "resource:///path/to/ui_file.ui" })
export class MyBox extends from(Gtk.Box, {
	count: Property.uint32(),
}) {
	readonly prefix = "Count:"

	protected _get_count_string(): string {
		return `${this.#prefix} ${this.count}`
	}
}

This might be problematic! If the UI file relies upon this callback for, say, a label's text, then it might run _get_count_string before prefix has a value, and will instead result in undefined.

Sadly, there is no way for GObjectify to account for this, so you must be diligent about when your callbacks run, and what your callbacks rely on!

Clone this wiki locally