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
- Keep all SetTextColor calls inside the same paint scope that owns the HDC; do not cache HDCs across messages.
- Confirm the color is built with a valid RGB() (avoid passing CLR_INVALID itself as the 'color').
- Run all GDI drawing on the thread that created the window to avoid DC lifetime races.
- 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
- Do not cache HDCs across messages or goroutines.
- Perform color setup immediately before the draw call in the same handler.
- Ensure ReleaseDC/EndPaint run after all drawing, exactly once.
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
- CancellablePromise does not support transparent subclassing.
- CancellablePromise.prototype.finally called on an invalid ob
- Invalid JSON passed to callback: ${e.message}. Message: ${in
- Callback '${callbackID}' not registered!!!
- Invalid JSON passed to Notify: ${notifyMessage}
AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15).
Data as JSON: /api/errors/b2f93e40cd15fa34.
Report an issue: GitHub.