Skip to main content
By default, overlayed() initializes everything the moment you call it - which is usually at import time, since the standard pattern is export const overlay = overlayed({ ... }) in a shared module. Sometimes that’s too early: you want the overlay to stay dormant until Electron is ready, until the user has logged in, or until they’ve opted into the overlay in your settings. This guide walks through splitting setup from initialization with init: false.
This guide covers the delayed-init workflow. For the option itself, see Delayed Initialization and the init option reference.

How Delayed Initialization Works

Four things to know before writing any code:
  1. overlayed() configures; init() activates. Normally overlayed() calls init() for you. With init: false, it validates your config and returns the overlay object without starting anything - no game process monitoring, no keybind listening, no windows, no events - until your code calls overlay.init().
  2. Before init(), the overlay object is locked. The only member you can touch is overlay.init. Accessing anything else (overlay.windows, overlay.on, even overlay.initialized) throws overlayed was called before initialized: <property>.
  3. init() is synchronous and one-shot. It takes no arguments and returns nothing - when the call returns, the overlay is fully initialized and overlay.initialized is true. Calling it a second time logs a warning and does nothing.
  4. Delaying doesn’t make you miss the game. Game detection starts at init() and also picks up games that are already running at that point - see the launch events guide. And gameReady supports late subscription: a listener attached after a game is ready is invoked immediately with that game’s data.

Splitting Setup from Initialization

1

Pass init: false

Keep your normal overlayed() setup - same modules, keybinds, and options - and add init: false. This stays safe to run at import time.
overlayed.ts
import { overlayed } from "@overlayed/app";
import * as Siege from "@overlayed/app/siege";
import electron from "electron";

export const overlay = overlayed({
	electron,
	applicationId: "YOUR_APPLICATION_ID",
	modules: [Siege.module()],
	keybinds: {
		toggleMainWindow: {
			keys: ["AltLeft", "KeyX"],
			mode: "toggle",
		},
	},
	init: false,
});
2

Call init() when your app is ready

Call overlay.init() at the moment of your choosing. init is safe to pass around as a reference - it’s the one member that works before initialization:
main.ts
import { app } from "electron";
import { overlay } from "./overlayed.ts";

app.whenReady().then(overlay.init);
3

Do all overlay work after init()

Register listeners and create windows only after init() has run. Since init() is synchronous, “after” just means later in the same function:
main.ts
import { app } from "electron";
import { overlay } from "./overlayed.ts";
import { AppWindow } from "./appWindow.ts";

app.whenReady().then(() => {
	overlay.init();

	const appWindow = new AppWindow();
	overlay.on("gameReady", () => appWindow.create());
	overlay.on("gameClose", () => appWindow.destroy());
});
Even if the user launched the game before this code ran, you’re covered: already-running games are detected once monitoring starts, and late gameReady subscribers are called immediately for games that are already ready.

Choosing the Init Moment

Where you put the overlay.init() call is the whole design decision. Two common patterns:
Initialize as soon as Electron is up. This keeps the overlay’s startup out of module-evaluation time without changing when it’s effectively available.
main.ts
import { app } from "electron";
import { overlay } from "./overlayed.ts";

app.whenReady().then(() => {
	overlay.init();
	startOverlayFeatures();
});

Keeping Pre-Init Code Safe

The locked-object rule bites in two specific places:
Any module-level code that touches the overlay runs at import time - before your deferred init() call - and throws. The two common offenders:
// Throws at import time: property access on a pre-init overlay
const { windows, keybinds } = overlay;

// Throws at import time: registering listeners at the top level of a module
overlay.on("gameReady", () => { ... });
Keep module scope down to importing overlay itself, and move every property access and listener registration into functions that run after init().
A layout that makes this hard to get wrong: overlayed.ts exports the un-initialized overlay, and a single startOverlayFeatures() function owns all listener registration and window creation. Your entrypoint calls overlay.init() and then startOverlayFeatures(), and nothing else ever touches overlay at module scope.

Waiting for the Game Instead

The description of this guide says “until the game is ready” - but note that you cannot literally wait for the game to launch before calling init(), because game detection itself doesn’t start until init() runs. No gameLaunch or gameReady will ever fire on an uninitialized overlay. If what you actually want is “do nothing user-facing until the game is ready”, you usually don’t need init: false at all. Initialize normally and lean on the built-in deferrals:
  • Create windows in a gameReady listener - see Game Launch & Close Events.
  • Hold off on consuming game events until you’re ready: events queue per game module until you call readyForGameEvents() - see Event Queuing.
Reserve init: false for gating the overlay on your app’s state (startup order, login, opt-in) rather than on the game’s state.

FAQ

Nothing starts: no process monitoring, no keybinds, no windows - none of the overlay’s subsystems come up. The first time any code accesses an overlay member other than init, it throws overlayed was called before initialized. There’s no timeout - the overlay waits indefinitely.
The second call logs the warning Overlayed already initialized and returns without doing anything. It won’t throw and won’t re-run initialization, so calling init() from two competing code paths is safe - just unnecessary.
No - init() is synchronous and returns void. There is nothing to await and no “initialized” event; when the call returns, the overlay is ready and overlay.initialized reads true. Code on the next line can safely create windows and register listeners.
No. init() takes no arguments - all configuration (modules, keybinds, universal mode, feature flags) is fixed by the options you passed to overlayed(). If an option needs to depend on runtime state, resolve that state before calling overlayed() itself.
No. Keybind listening is part of initialization, like everything else - there’s no partial mode where some subsystems run early. Windows, keybinds, game events, notices, and feature flags all become available together when init() runs.
You won’t receive events from before init() - detection wasn’t running, so none were produced for you. What you don’t lose is the session: a game already running at init() time is still detected and goes through the normal launch flow. After that, in-game events queue per module until you call readyForGameEvents(), so your own startup code has slack too. See Event Queuing.
Some code accessed overlay.X before overlay.init() ran. The property name in the message tells you what was touched - search for that access. It’s almost always module-level code: a top-level overlay.on(...), or destructuring the overlay object at import time. Move the access into code that runs after init().
No - despite the similar wording, that error is about your local development environment, not delayed init. It means the .overlayed metadata cache hasn’t been created yet; run overlayed init (the CLI command) to set it up. See Local Development.

Next Steps

Delayed Initialization Reference

The init option in the advanced reference.

Game Launch & Close Events

React to gameLaunch, gameReady, and gameClose - including late subscription.

Game Events

Event queuing and readyForGameEvents().

Overlayed Options

Every option you can pass to overlayed().