wailsapp/wails · critical · Error
${await response.text()}
Error message
${await response.text()} What it means
GdiplusStartup initializes the GDI+ library, returning a Status code where Ok(0) means success. The w32 wrapper panics with 'GdiplusStartup failed with status <name>' for any non-Ok status. In practice GdiplusStartup almost never fails unless COM/GDI state is broken, the token storage is corrupt, or initialization is attempted repeatedly/unbalanced against GdiplusShutdown. The status name in the message (from GetGpStatus) identifies which condition fired, e.g. GdiplusNotInitialized or OutOfMemory.
Source
Thrown at v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts:179
response = await sendChunked(url, headers, bodyStr);
} else {
response = await fetch(url, { method: 'POST', headers, body: bodyStr });
}
if (!response.ok) {
const ct = response.headers.get("Content-Type");
if (ct?.includes("application/json")) {
const json: CallErrorType = await response.json();
let err;
switch (json.kind) {
case "ReferenceError": err = new ReferenceError(json.message); break;
case "TypeError": err = new TypeError(json.message); break;
case "RuntimeError": err = new RuntimeError(json.message); break;
default: err = new Error(json.message);
}
err.cause = json.cause;
throw err
}
throw new Error(await response.text());
}
if ((response.headers.get("Content-Type")?.indexOf("application/json") ?? -1) !== -1) {
return response.json();
} else {
return response.text();
}
}
// sendChunked splits a large serialised request body into CHUNK_THRESHOLD-sized
// byte chunks and sends them serially. Encoding to UTF-8 bytes before slicing
// prevents corruption of non-BMP characters (surrogate pairs) that would occur
// when splitting at JavaScript string indices. The Go transport assembles the
// raw bytes before processing. Only the final chunk's response carries the RPC result.
async function sendChunked(url: URL, headers: Record<string, string>, bodyStr: string): Promise<Response> {
const chunkId = nanoid();
const bodyBytes = new TextEncoder().encode(bodyStr);
const totalChunks = Math.ceil(bodyBytes.length / CHUNK_THRESHOLD);View on GitHub (pinned to 0e754b1b40)
Solutions
- Initialize GDI+ exactly once at feature start and pair it with a single GdiplusShutdown at feature end (sync.Once or explicit lifecycle owner).
- Serialize startup/shutdown with a mutex if multiple code paths can trigger GDI+ use.
- Read the status name in the panic message: OutOfMemory points at memory pressure, not API misuse — free large buffers first.
- Verify the GdiplusStartupInput struct was built with a valid GdiplusVersion (1).
- If it fails only in a long-running app, check the GDI object count for exhaustion feeding into GDI+ init.
Example fix
// before
func encode(img []byte){
var in w32.GdiplusStartupInput; in.GdiplusVersion = 1
w32.GdiplusStartup(&in, nil) // called per request; unbalanced with shutdown
... // occasional second startup -> panic
}
// after
var gpOnce sync.Once
func ensureGdiplus(){
gpOnce.Do(func(){
var in w32.GdiplusStartupInput; in.GdiplusVersion = 1
w32.GdiplusStartup(&in, nil)
})
} Defensive patterns
Strategy: validation
Try / catch
defer func() {
if r := recover(); r != nil {
if msg, _ := r.(string); strings.HasPrefix(msg, "GdiplusStartup") {
err = fmt.Errorf("gdiplus init: %v", r)
return
}
panic(r)
}
}()
w32.GdiplusStartup(&input, nil) Prevention
- Initialize GDI+ once per process via sync.Once and shut down once at exit.
- Always set GdiplusStartupInput.GdiplusVersion = 1.
- Read the status name in the panic message — OutOfMemory means memory pressure, not API misuse.
When it happens
Trigger: Calling GdiplusStartup a second time without GdiplusShutdown in between on the same token variable; calling startup after the process has corrupted GDI state through handle leaks; running it on a thread whose apartment state conflicts with prior COM initialization; out-of-memory conditions from enormous bitmap allocations.
Common situations: An image-decoding feature (thumbnailer, screenshot encoder) that initializes GDI+ per request but shuts down only sometimes; app-level OOM from loading huge images that then surfaces in the next GDI+ init; initialization racing between two goroutines calling into w32 concurrently.
Related errors
- Invalid JSON passed to callback: ${e.message}. Message: ${in
- Callback '${callbackID}' not registered!!!
- Invalid JSON passed to Notify: ${notifyMessage}
- CancellablePromise does not support transparent subclassing.
- CancellablePromise.prototype.then called on an invalid objec
AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15).
Data as JSON: /api/errors/d04f5c9137e6b772.
Report an issue: GitHub.