wailsapp/wails · error · Error

Callback '${callbackID}' not registered!!!

Error message

Callback '${callbackID}' not registered!!!

What it means

CreateCompatibleDC wraps gdi32.CreateCompatibleDC, which creates a memory device context matching a given DC (or the screen when passed 0). The w32 wrapper panics with 'Create compatible DC failed' when the API returns NULL, meaning GDI could not allocate a new DC. This most commonly happens under GDI handle exhaustion (10,000 GDI objects per process default) or when the source HDC parameter is itself invalid.

Source

Thrown at v2/internal/frontend/runtime/desktop/calls.js:174

 * @export
 * @param {string} incomingMessage
 */
export function Callback(incomingMessage) {
	// Parse the message
	let message;
	try {
		message = JSON.parse(incomingMessage);
	} catch (e) {
		const error = `Invalid JSON passed to callback: ${e.message}. Message: ${incomingMessage}`;
		runtime.LogDebug(error);
		throw new Error(error);
	}
	let callbackID = message.callbackid;
	let callbackData = callbacks[callbackID];
	if (!callbackData) {
		const error = `Callback '${callbackID}' not registered!!!`;
		console.error(error); // eslint-disable-line
		throw new Error(error);
	}
	clearTimeout(callbackData.timeoutHandle);

	delete callbacks[callbackID];

	if (message.error) {
		const err = message.error instanceof Error ? message.error : new Error(message.error);
		callbackData.reject(err);
	} else {
		callbackData.resolve(message.result);
	}
}

View on GitHub (pinned to 0e754b1b40)

Solutions

  1. Audit every CreateCompatibleDC call site and guarantee DeleteDC is called on all paths, including errors (use defer immediately after creation succeeds).
  2. Watch the process GDI object count in Task Manager / Process Explorer while reproducing; a steady climb pinpoints the leaking code path.
  3. Cache and reuse a single memory DC per window instead of per-frame creation.
  4. Check the source HDC passed in is non-zero and alive; pass 0 to create a screen-compatible DC when no source is needed.
  5. If the cap is genuinely reached by design, raise the GDI process limit via SetProcessGDIObjectCount or the registry GDIProcessHandleQuota, but treat that as a last resort after fixing leaks.

Example fix

// before
hdcMem := w32.CreateCompatibleDC(hdc)
img := renderFrame(hdcMem)
// no DeleteDC on the error path -> GDI handle leak -> eventual panic

// after
hdcMem := w32.CreateCompatibleDC(hdc)
defer w32.DeleteDC(hdcMem)
img := renderFrame(hdcMem)
Defensive patterns

Strategy: validation

Try / catch

func newMemDC(hdc w32.HDC) (w32.HDC, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("CreateCompatibleDC: %v (GDI objects: check Task Manager)", r)
		}
	}()
	return w32.CreateCompatibleDC(hdc), nil
}

Prevention

When it happens

Trigger: Creating memory DCs in a loop without calling DeleteDC on each one, leaking GDI handles until the process hits the GDI object cap; passing an HDC obtained from a destroyed window or an already-deleted DC as the source; running many offscreen-buffer allocations concurrently during heavy animation or screenshot loops.

Common situations: Rendering thumbnails or frames into compatible bitmaps at high frequency with a missing DeleteDC in an error path; long-running Wails apps on Windows whose GDI handle count slowly climbs (visible in Task Manager's 'GDI objects' column); the leak only manifests as this panic hours into a session.

Related errors


AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15). Data as JSON: /api/errors/3bfa15171e4e44fe. Report an issue: GitHub.