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

  1. Initialize GDI+ exactly once at feature start and pair it with a single GdiplusShutdown at feature end (sync.Once or explicit lifecycle owner).
  2. Serialize startup/shutdown with a mutex if multiple code paths can trigger GDI+ use.
  3. Read the status name in the panic message: OutOfMemory points at memory pressure, not API misuse — free large buffers first.
  4. Verify the GdiplusStartupInput struct was built with a valid GdiplusVersion (1).
  5. 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

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


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