Skip to content

Bindings

Introduction

One of the key features of Wails is the ability to seamlessly integrate backend Go code with the frontend, enabling efficient communication between the two. This can be done manually by sending messages between the frontend and backend, but this can be cumbersome and error-prone, especially when dealing with complex data types.

The bindings generator in Wails v3 simplifies this process by automatically generating JavaScript or TypeScript functions and models that reflect the methods and data structures defined in your Go code. This means you can write your backend logic in Go and easily expose it to the frontend without the need for manual binding or complex integration.

This guide is designed to help you understand and utilize this powerful binding tool.

Core Concepts

In Wails v3, services can be added to your application. These services act as a bridge between the backend and frontend, allowing you to define methods and state that can be accessed and manipulated from the frontend.

Services

  1. Services can hold state and expose methods that operate on that state.
  2. Services can be used similar to controllers in HTTP web applications or as services.
  3. Only public methods on the service are bound, following Go’s convention.

Here’s a simple example of how you can define a service and add it to your Wails application:

package main
import (
"log"
"github.com/wailsapp/wails/v3/pkg/application"
)
type GreetService struct {}
func (g *GreetService) Greet(name string) string {
return "Hello " + name
}
func main() {
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&GreetService{}),
},
})
// ....
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

In this example, we define a GreetService services with a public Greet method. The Greet method takes a name parameter and returns a greeting string.

We then create a new Wails application using application.New and add the GreetService service to the application using the Services option in the application.Options. The application.NewService method must always be given an instance of the service struct, not the service struct type itself.

Generating the Bindings

By binding the struct, Wails is able to generate the necessary JavaScript or TypeScript code by running the following command in the project directory:

Terminal window
wails3 generate bindings

The bindings generator will scan the project and dependencies for anything that needs generating. Note: It will take longer the very first time you run the bindings generator, as it will be building up a cache of packages to scan. You should see output similar to the following:

Terminal window
% wails3 generate bindings
INFO 347 Packages, 1 Service, 1 Method, 0 Enums, 0 Models in 1.981036s.
INFO Output directory: /Users/me/myproject/frontend/bindings

If we look in the frontend/bindings directory, we should see the following files:

  • Directoryfrontend/bindings
    • Directorychangeme
      • greetservice.js
      • index.js

NOTE: The changeme directory is the name of the module defined in go.mod and is used to namespace the generated files.

The generated greetservice.js file contains the JavaScript code that mirrors the Go struct and its methods:

greetservice.js
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import { Call as $Call, Create as $Create } from "@wailsio/runtime";
/**
* @param {string} name
* @returns {Promise<string> & { cancel(): void }}
*/
export function Greet(name) {
let $resultPromise = /** @type {any} */ ($Call.ByID(1411160069, name));
return $resultPromise;
}

As you can see, it also generates all the necessary JSDoc type information to ensure type safety in your frontend code.

Using the Bindings

You can import and use this file in your frontend code to interact with the backend.

import { Greet } from "./bindings/changeme/greetservice.js";
console.log(Greet("Alice")); // Output: Hello Alice

Binding Models

In addition to binding methods, you can also use structs as input or output parameters in your bound methods. When structs are used as parameters, Wails generates corresponding JavaScript versions of those types.

Let’s extend the previous example to use a Person type that has a Name field:

package main
import (
"github.com/wailsapp/wails/v3/pkg/application"
"log"
)
// Person defines a person
type Person struct {
// Name of the person
Name string
}
type GreetService struct{}
func (g *GreetService) Greet(person Person) string {
return "Hello " + person.Name
}
func main() {
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&GreetService{}),
},
})
// ....
app.NewWebviewWindow()
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

In this updated example, we define a Person struct with a Name field. The Greet method in the GreetService service now takes a Person as an input parameter.

When you run the bindings generator, Wails will generate a corresponding JavaScript Person type that mirrors the Go struct. This allows you to create instances of the Person type in your frontend code and pass them to the bound Greet method.

If we run the bindings generator again, we should see the following output:

Terminal window
% wails3 generate bindings
INFO Processed: 347 Packages, 1 Service, 1 Method, 0 Enums, 1 Model in 1.9943997s.
INFO Output directory: /Users/me/myproject/frontend/bindings

In the frontend/bindings/changeme directory, you should see a new models.js file containing the following code:

models.js
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import { Create as $Create } from "@wailsio/runtime";
/**
* Person defines a person
*/
export class Person {
/**
* Creates a new Person instance.
* @param {Partial<Person>} [$$source = {}] - The source object to create the Person.
*/
constructor($$source = {}) {
if (!("Name" in $$source)) {
/**
* Name of the person
* @member
* @type {string}
*/
this["Name"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new Person instance from a string or object.
* @param {any} [$$source = {}]
* @returns {Person}
*/
static createFrom($$source = {}) {
let $$parsedSource =
typeof $$source === "string" ? JSON.parse($$source) : $$source;
return new Person(/** @type {Partial<Person>} */ ($$parsedSource));
}
}

The Person class is generated with a constructor that takes an optional source parameter, which allows you to create a new Person instance from an object. It also has a static createFrom method that can create a Person instance from a string or object.

You may also notice that comments in the Go struct are kept in the generated JavaScript code! This can be helpful for understanding the purpose of the fields and methods in the generated models and should be picked up by your IDE.

Using Bound Models

Here’s an example of how you can use the generated JavaScript Person type in your frontend code:

import { Greet } from "./bindings/changeme/greetservice.js";
import { Person } from "./bindings/changeme/models.js";
const resultElement = document.getElementById("result");
async function doGreet() {
let person = new Person({ Name: document.getElementById("name").value });
if (!person.Name) {
person.Name = "anonymous";
}
resultElement.innerText = await Greet(person);
}

In this example, we import the generated Person type from the models module. We create a new instance of Person, set its Name property, and pass it to the Greet method.

Using bound models allows you to work with complex data structures and seamlessly pass them between the frontend and backend of your Wails application.

Index files

The generator outputs an additional index.js file that re-exports all services and models:

index.js
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as GreetService from "./greetservice.js";
export {
GreetService
};
export {
Person
} from "./models.js";

You can take advantage of this feature to aggregate import statements for multiple services and models. If you are building your frontend with a bundler, which is the default for most project templates, you can also simplify the import path:

import { GreetService, Person } from "./bindings/changeme";
await GreetService.Greet(new Person(/* ... */));

Using Typescript

To generate TypeScript bindings instead of JavaScript, you can use the -ts flag:

Terminal window
% wails3 generate bindings -ts

This will generate TypeScript files in the frontend/bindings directory:

  • Directoryfrontend/bindings
    • Directorymain
      • greetservice.ts
      • index.ts
      • models.ts

The generated files include greetservice.ts, which contains the TypeScript code for the bound struct and its methods, and models.ts, which contains the TypeScript types for the bound models:

GreetService.ts
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import { Call as $Call, Create as $Create } from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
export function Greet(
person: $models.Person,
): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(1411160069, person) as any;
return $resultPromise;
}
models.ts
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* Person defines a person
*/
export class Person {
/**
* Name of the person
*/
"Name": string;
/** Creates a new Person instance. */
constructor(source: Partial<Person> = {}) {
if (!("Name" in source)) {
this["Name"] = "";
}
Object.assign(this, source);
}
/** Creates a new Person instance from a string or object. */
static createFrom(source: string | object = {}): Person {
let parsedSource = typeof source === "string" ? JSON.parse(source) : source;
return new Person(parsedSource as Partial<Person>);
}
}

Using TypeScript bindings provides type safety and improved IDE support when working with the generated code in your frontend.

Using context.Context

When defining service methods in Go, you can include context.Context as the first parameter. The runtime will automatically provide a context when the method is called from the frontend.

The context provides several powerful features:

  1. Cancellation Support: Long-running operations can be cancelled from the frontend, which will raise an error through the Promise chain.

  2. Window Information: You can determine which window made the call using the context key application.WindowKey.

Here are some examples:

// Basic context usage with cancellation
func (s *MyService) LongRunningTask(ctx context.Context, input string) (string, error) {
select {
// Check if the context has been cancelled from the frontend
case <-ctx.Done():
return "", ctx.Err()
default:
// Process task
return "completed", nil
}
}
// Getting caller window information
func (s *MyService) WindowAwareMethod(ctx context.Context) (string, error) {
window := ctx.Value(application.WindowKey).(application.Window)
return fmt.Sprintf("Called from window: %s (ID: %s)", window.Name(), window.ID()), nil
}

From the frontend, these methods can be called normally. If you need to cancel a long-running operation, you can call the special cancel method on the promise and it will reject immediately with a special cancellation error; the Go context will be cancelled and the actual result of the call will be discarded:

// Call the method
const promise = MyService.LongRunningTask("input");
// Cancel it later if needed
// This will cause the context to be cancelled in the Go method
promise.cancel();

In fact, the runtime returns a special promise wrapper that provides cancellation support for arbitrarily long promise chains. For example:

import { CancelError } from "@wailsio/runtime";
// Call the method and process its output
const promise = MyService.LongRunningTask("input").then((result) => {
console.log(result);
}).catch((err) => {
if (err instanceof CancelError) {
console.log("Cancelled.", err.cause);
} else {
console.error("Failed.", err);
}
});
// Later...
// cancel() accepts an optional cause parameter
// that will be attached to the cancellation error:
promise.cancel("I'm tired of waiting!").then(() => {
// Cancellation has been requested successfully
// and all handlers attached above have run.
console.log("Ready for the next adventure!");
});

The cancel method returns a promise that fulfills always (and never rejects) after the cancellation request has been submitted successfully and all previously attached handlers have run.

The approach discussed above requires storing and chaining promises manually, which can be cumbersome for code written in async/await style. If you target plaforms that support the AbortController/AbortSignal idiom, you can call the cancelOn method and tie call cancellation to an AbortSignal instead:

async function callBinding(signal) {
try {
await MyService.LongRunningTask("input").cancelOn(signal);
} catch (err) {
if (err instanceof CancelError) {
console.log("Cancelled! Cause: ", err.cause);
} else {
console.error("Failed! Error: ", err);
}
}
}
let controller = new AbortController();
callBinding(controller.signal);
// Later...
controller.abort("I'm tired of waiting!");

Handling errors

As you may have noticed above, bound methods can return errors, which are handled specially. When a result field has type error, it is omitted by default from the values returned to JS. When such a field is non-nil, the promise rejects with a RuntimeError exception that wraps the Go error message:

func (*MyService) FailingMethod(name string) error {
return fmt.Errorf("Welcome to an imperfect world, %s", name)
}
import { MyService } from './bindings/changeme';
try {
await MyService.FailingMethod("CLU")
} catch (err) {
if (err.name === 'RuntimeError') {
console.log(err.message); // Prints 'Welcome to an imperfect world, CLU'
}
}

The exception will be an instance of the Call.RuntimeError class from the wails runtime, hence you can also test its type like this:

import { Call } from '@wailsio/runtime';
try {
// ...
} catch (err) {
if (err instanceof Call.RuntimeError) {
// ...
}
}

If the Go error value supports JSON marshaling, the exception’s cause property will hold the marshaled version of the error:

type ImperfectWorldError struct {
Name string `json:"name"`
}
func (err *ImperfectWorldError) Error() {
return fmt.Sprintf("Welcome to an imperfect world, %s", err.Name)
}
func (*MyService) FailingMethod(name string) error {
return &ImperfectWorldError{
Name: name,
}
}
import { MyService } from './bindings/changeme';
try {
await MyService.FailingMethod("CLU")
} catch (err) {
if (err.name === 'RuntimeError') {
console.log(err.cause.name); // Prints 'CLU'
}
}

Generally, many Go error values will only have limited or no support for marshaling to JSON. If you so wish, you can customise the value provided as cause by specifying either a global or per-service error marshaling function:

app := application.New(application.Options{
MarshalError: func(err error) []byte {
// ...
},
Services: []application.Service{
application.NewServiceWithOptions(&MyService{}, application.ServiceOptions{
MarshalError: func(err error) []byte {
// ...
},
}),
},
})

Per-service functions override the global function, which in turn overrides the default behaviour of using json.Marshal. If a marshaling function returns nil, it falls back to the outer function: per-service functions fall back to the global function, which in turn falls back to the default behaviour.

Here’s an example marshaling function that unwraps path errors and reports the file path:

app := application.New(application.Options{
MarshalError: func(err error) []byte {
var perr *fs.PathError
if !errors.As(err, &perr) {
// Not a path error, fall back to default handling.
return nil
}
// Marshal path string
path, err := json.Marshal(&perr.Path)
if err != nil {
// String marshaling failed, fall back to default handling.
return nil
}
return []byte(fmt.Sprintf(`{"path":%s}`, path))
},
})

Binding call promises may also reject with a TypeError when the method has been passed the wrong number of arguments, when the conversion of arguments from JSON to their Go types fails, or when the conversion of results to JSON fails. These problems will usually be caught early by the type system. If your code typechecks but you still get type errors, it might be that some of your Go types are not supported by the encoding/json package: look for warnings from the binding generator to catch these.