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
- Audit every CreateCompatibleDC call site and guarantee DeleteDC is called on all paths, including errors (use defer immediately after creation succeeds).
- Watch the process GDI object count in Task Manager / Process Explorer while reproducing; a steady climb pinpoints the leaking code path.
- Cache and reuse a single memory DC per window instead of per-frame creation.
- Check the source HDC passed in is non-zero and alive; pass 0 to create a screen-compatible DC when no source is needed.
- 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
- Pair every CreateCompatibleDC with an immediate defer DeleteDC.
- Cache one memory DC per window instead of allocating per frame.
- Track GDI object count during development to catch leaks long before the 10,000 cap.
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
- Invalid JSON passed to callback: ${e.message}. Message: ${in
- Invalid JSON passed to Notify: ${notifyMessage}
- CancellablePromise does not support transparent subclassing.
- CancellablePromise.prototype.then called on an invalid objec
- CancellablePromise.prototype.finally called on an invalid ob
AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15).
Data as JSON: /api/errors/3bfa15171e4e44fe.
Report an issue: GitHub.