Skip to content

Android binding transport drops CallError cause and custom MarshalError payload #6053

Description

@ryan053857

Wails version family

v3

Exact Wails version

v3.0.0-beta.15

The same transport behaviour is still present on master as of 2026-08-29.

Operating System

Android (experimental), built on macOS arm64 and reproduced on an Android 15 arm64 emulator.

Description

The Android JavascriptInterface binding transport discards the structured CallError produced for a bound Go method. In particular, an application's ServiceOptions.MarshalError payload is correctly stored in CallError.Cause, but Android reduces the wrapped error to err.Error() and the JavaScript runtime reconstructs only a plain Error.

This makes Android binding semantics differ from the HTTP transport used on other platforms: callers cannot inspect error.cause, so structured application errors and session-routing metadata are lost.

The loss occurs in two places:

  1. pkg/application/application_android.go calls fail(err.Error()) in handleRuntimeCallForAndroid, returning only {"ok":false,"error":"..."}.
  2. The Android branch of @wailsio/runtime rejects with new Error(envelope.error), without restoring kind, message, or cause from a CallError.

This is distinct from #5723 / #5858. Those address non-JSON responses in the Android HTTP fallback path and intentionally wrap the error as a string matching the existing JavascriptInterface envelope. They do not preserve CallError.Cause.

To Reproduce

Register a service with a custom error marshaler:

type AppError struct {
    State   string `json:"state"`
    Message string `json:"message"`
}

func (e *AppError) Error() string { return e.State + ": " + e.Message }

func marshalError(err error) []byte {
    var appErr *AppError
    if !errors.As(err, &appErr) {
        return nil
    }
    payload, _ := json.Marshal(appErr)
    return payload
}

type Service struct{}

func (*Service) Fail() error {
    return &AppError{State: "login", Message: "Please log in first."}
}

application.NewServiceWithOptions(&Service{}, application.ServiceOptions{
    MarshalError: marshalError,
})

Call the generated binding and inspect the rejection:

try {
  await Fail()
} catch (error) {
  console.log(error.name)
  console.log(error.message)
  console.log(error.cause)
}

On Android, the result is equivalent to:

Error
Binding call failed: Bound method returned an error: login: Please log in first.
undefined

The custom structured payload was present in the original CallError.Cause, but is no longer available to JavaScript.

Expected behaviour

Android should preserve the same bound-method error semantics as the standard HTTP transport:

  • return the complete CallError (kind, message, and cause) in the Android response envelope;
  • reconstruct the corresponding JavaScript error, including error.cause;
  • preserve arbitrary JSON returned by ServiceOptions.MarshalError.

For backward compatibility, transport/internal failures could continue accepting a string error, while bound CallError values use a structured error object.

Screenshots

Not applicable.

Attempted Fixes

  • Confirmed that MarshalError is invoked and that bindings.go assigns its JSON to CallError.Cause.
  • Confirmed that the Android loss happens after HandleRuntimeCallWithIDs returns the wrapped CallError.
  • Checked current master; it still serializes only err.Error() and creates a plain JavaScript Error.
  • Reviewed Android HTTP Fallback Returns Non-JSON Error Responses #5723 and fix(android): wrap runtime-call HTTP fallback errors in JSON envelope #5858; they fix an adjacent HTTP fallback JSON-parsing problem but do not preserve structured causes.
  • Application-side parsing of the final message can recover a textual prefix such as login, but cannot recover arbitrary structured fields and is therefore not an equivalent workaround.

System Details

Wails CLI      | v3.0.0-beta.15
Go Version     | go1.27.0
Host OS        | macOS 26.5.2 (25F84), arm64, Apple Silicon
Android SDK    | /opt/homebrew/share/android-commandlinetools
Android NDK    | 26.3.11579264
Java           | OpenJDK 21.0.12
Emulator       | Android 15, Google APIs, arm64-v8a
npm            | 11.16.0

wails3 doctor reports no issues.

Additional context

A complete fix likely needs both sides of the Android transport to change:

  • Go: detect the wrapped *application.CallError (for example with errors.As) and encode it in the Android envelope instead of calling only err.Error().
  • JavaScript runtime: when envelope.error is structured, construct ReferenceError, TypeError, or RuntimeError consistently with the HTTP transport and assign its cause; continue supporting legacy string envelopes.

Regression coverage should include a bound method using a custom MarshalError and assert that an Android runtime rejection exposes the original structured cause.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions