wailsapp/wails · error · TypeError

CancellablePromise.prototype.then called on an invalid objec

Error message

CancellablePromise.prototype.then called on an invalid object.

What it means

SetTextColor sets the foreground color for text and vector drawing on a DC and returns the previous color. The w32 wrapper panics with 'SetTextColor failed' when the return equals CLR_INVALID (0xFFFFFFFF), the documented failure value. An invalid or deleted HDC is the usual cause; the color value itself is almost never the problem because nearly every 32-bit value is a legal COLORREF.

Source

Thrown at v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts:370

     * The returned promise is hooked up to propagate cancellation requests up the chain, but not down:
     *
     *   - if the parent promise is cancelled, the `onrejected` handler will be invoked with a `CancelError`
     *     and the returned promise _will resolve regularly_ with its result;
     *   - conversely, if the returned promise is cancelled, _the parent promise is cancelled too;_
     *     the `onrejected` handler will still be invoked with the parent's `CancelError`,
     *     but its result will be discarded
     *     and the returned promise will reject with a `CancelError` as well.
     *
     * The promise returned from {@link cancel} will fulfill only after all attached handlers
     * up the entire promise chain have been run.
     *
     * If either callback returns a cancellable promise,
     * cancellation requests will be diverted to it,
     * and the specified `oncancelled` callback will be discarded.
     */
    then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1> | CancellablePromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2> | CancellablePromiseLike<TResult2>) | undefined | null, oncancelled?: CancellablePromiseCanceller): CancellablePromise<TResult1 | TResult2> {
        if (!(this instanceof CancellablePromise)) {
            throw new TypeError("CancellablePromise.prototype.then called on an invalid object.");
        }

        // NOTE: TypeScript's built-in type for then is broken,
        // as it allows specifying an arbitrary TResult1 != T even when onfulfilled is not a function.
        // We cannot fix it if we want to CancellablePromise to implement PromiseLike<T>.

        if (!isCallable(onfulfilled)) { onfulfilled = identity as any; }
        if (!isCallable(onrejected)) { onrejected = thrower; }

        if (onfulfilled === identity && onrejected == thrower) {
            // Shortcut for trivial arguments.
            return new CancellablePromise((resolve) => resolve(this as any));
        }

        const barrier: Partial<PromiseWithResolvers<void>> = {};
        this[barrierSym] = barrier;

        return new CancellablePromise<TResult1 | TResult2>((resolve, reject) => {

View on GitHub (pinned to 0e754b1b40)

Solutions

  1. Keep all SetTextColor calls inside the same paint scope that owns the HDC; do not cache HDCs across messages.
  2. Confirm the color is built with a valid RGB() (avoid passing CLR_INVALID itself as the 'color').
  3. Run all GDI drawing on the thread that created the window to avoid DC lifetime races.
  4. If it panics only at exit, add an is-closing guard around paint logic.

Example fix

// before
hdc := w32.GetDC(hwnd)
queueDraw(func(){ w32.SetTextColor(w32.HDC(savedHdc), c) }) // savedHdc released already

// after
hdc := w32.GetDC(hwnd)
w32.SetTextColor(hdc, c)
drawTextNow(hdc)
w32.ReleaseDC(hwnd, hdc)
Defensive patterns

Strategy: validation

Try / catch

defer func() {
	if r := recover(); r != nil {
		if msg, _ := r.(string); strings.HasPrefix(msg, "SetTextColor failed") {
			log.Printf("color setup skipped: DC likely released")
			return
		}
		panic(r)
	}
}()
w32.SetTextColor(hdc, color)

Prevention

When it happens

Trigger: Calling SetTextColor with an HDC after EndPaint/ReleaseDC/DeleteDC; calling it on a DC owned by another thread while GDI internal state is being torn down; stacking color setup calls in a helper that outlives the paint cycle.

Common situations: Custom WM_PAINT handlers that stash the HDC for later use after the handler returns; drawing during app shutdown when the window DC is gone; color-setup helpers called from a goroutine rather than the UI thread.

Related errors


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