Skip to content

Commit e7c134d

Browse files
[v3] Late service registration and error handling overhaul (#4066)
* Add service registration method * Fix error handling and formatting in messageprocessor * Add configurable error handling * Improve error strings * Fix service shutdown on macOS * Add post shutdown hook * Better fatal errors * Add startup/shutdown sequence tests * Improve debug messages * Update JS runtime * Update docs * Update changelog * Fix log message in clipboard message processor Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Remove panic in RegisterService * Fix linux tests (hopefully) * Fix error formatting everywhere * Fix typo in windows webview * Tidy example mods * Set application name in tests * Fix ubuntu test workflow * Cleanup template test pipeline * Fix dev build detection on Go 1.24 * Update template go.mod/sum to Go 1.24 * Remove redundant caching in template tests * Final format string cleanup * Fix wails3 tool references * Fix legacy log calls * Remove formatJS and simplify format strings * Fix indirect import --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 5059adc commit e7c134d

File tree

75 files changed

+2593
-954
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+2593
-954
lines changed

.github/workflows/build-and-test-v3.yml

+46-39
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
fail-fast: false
3131
matrix:
3232
os: [windows-latest, macos-latest, ubuntu-latest]
33-
go-version: [1.23]
33+
go-version: [1.24]
3434

3535
steps:
3636
- name: Checkout code
@@ -40,7 +40,7 @@ jobs:
4040
uses: awalsh128/cache-apt-pkgs-action@latest
4141
if: matrix.os == 'ubuntu-latest'
4242
with:
43-
packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config
43+
packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config xvfb x11-xserver-utils at-spi2-core xdg-desktop-portal-gtk
4444
version: 1.0
4545

4646
- name: Setup Go
@@ -66,11 +66,25 @@ jobs:
6666
working-directory: ./v3
6767
run: go test -v ./...
6868

69-
- name: Run tests (!mac)
70-
if: matrix.os != 'macos-latest'
69+
- name: Run tests (windows)
70+
if: matrix.os == 'windows-latest'
7171
working-directory: ./v3
7272
run: go test -v ./...
7373

74+
- name: Run tests (ubuntu)
75+
if: matrix.os == 'ubuntu-latest'
76+
working-directory: ./v3
77+
run: >
78+
xvfb-run --auto-servernum
79+
sh -c '
80+
dbus-update-activation-environment --systemd --all &&
81+
go test -v ./...
82+
'
83+
84+
- name: Typecheck binding generator output
85+
working-directory: ./v3
86+
run: task generator:test:check
87+
7488
test_js:
7589
name: Run JS Tests
7690
needs: check_approval
@@ -105,59 +119,52 @@ jobs:
105119
matrix:
106120
os: [ubuntu-latest, windows-latest, macos-latest]
107121
template:
108-
[
109-
svelte,
110-
svelte-ts,
111-
vue,
112-
vue-ts,
113-
react,
114-
react-ts,
115-
preact,
116-
preact-ts,
117-
lit,
118-
lit-ts,
119-
vanilla,
120-
vanilla-ts,
121-
]
122-
go-version: [1.23]
122+
- svelte
123+
- svelte-ts
124+
- vue
125+
- vue-ts
126+
- react
127+
- react-ts
128+
- preact
129+
- preact-ts
130+
- lit
131+
- lit-ts
132+
- vanilla
133+
- vanilla-ts
134+
go-version: [1.24]
123135
steps:
124136
- name: Checkout
125137
uses: actions/checkout@v4
126138

139+
- name: Install linux dependencies
140+
uses: awalsh128/cache-apt-pkgs-action@latest
141+
if: matrix.os == 'ubuntu-latest'
142+
with:
143+
packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config
144+
version: 1.0
145+
127146
- name: Setup Go
128147
uses: actions/setup-go@v5
129148
with:
130149
go-version: ${{ matrix.go-version }}
131150
cache-dependency-path: "v3/go.sum"
132151

133-
- name: Setup Golang caches
134-
uses: actions/cache@v4
135-
with:
136-
path: |
137-
~/.cache/go-build
138-
~/go/pkg/mod
139-
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
140-
restore-keys: |
141-
${{ runner.os }}-golang-
142-
143-
- name: Install linux dependencies
144-
uses: awalsh128/cache-apt-pkgs-action@latest
145-
if: matrix.os == 'ubuntu-latest'
152+
- name: Install Task
153+
uses: arduino/setup-task@v2
146154
with:
147-
packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config
148-
version: 1.0
155+
version: 3.x
156+
repo-token: ${{ secrets.GITHUB_TOKEN }}
149157

150158
- name: Build Wails3 CLI
159+
working-directory: ./v3
151160
run: |
152-
cd ./v3/cmd/wails3
153-
go install
154-
wails3 -help
161+
task install
162+
wails3 doctor
155163
156164
- name: Generate template '${{ matrix.template }}'
157165
run: |
158-
go install github.com/go-task/task/v3/cmd/task@latest
159166
mkdir -p ./test-${{ matrix.template }}
160167
cd ./test-${{ matrix.template }}
161168
wails3 init -n ${{ matrix.template }} -t ${{ matrix.template }}
162169
cd ${{ matrix.template }}
163-
wails3 build
170+
wails3 build

docs/src/content/docs/changelog.mdx

+11
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5454
- Add `//wails:internal` directive on services and models to allow for types that are exported in Go but not in JS/TS by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045)
5555
- Add binding generator support for constants of alias type to allow for weakly typed enums by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045)
5656
- Add support for macOS 15 "Sequoia" to `OSInfo.Branding` for improved OS version detection in [#4065](https://github.com/wailsapp/wails/pull/4065)
57+
- Add `PostShutdown` hook for running custom code after the shutdown process completes by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
58+
- Add `FatalError` struct to support detection of fatal errors in custom error handlers by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
59+
- Standardise and document service startup and shutdown order by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
60+
- Add test harness for application startup/shutdown sequence and service startup/shutdown tests by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
61+
- Add `RegisterService` method for registering services after the application has been created by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
62+
- Add `MarshalError` field in application and service options for custom error handling in binding calls by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
5763

5864
### Fixed
5965

@@ -81,6 +87,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8187
- Suppressed warnings for services that define lifecycle or http methods but no other bound methods by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045)
8288
- Fixed non-React templates failing to display Hello World footer when using light system colour scheme by [@marcus-crane](https://github.com/marcus-crane) in [#4056](https://github.com/wailsapp/wails/pull/4056)
8389
- Fixed hidden menu items on macOS by [@leaanthony](https://github.com/leaanthony)
90+
- Fixed handling and formatting of errors in message processors by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
91+
-  Fixed skipped service shutdown when quitting application by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
8492

8593
### Changed
8694

@@ -98,6 +106,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
98106
- Update copyright date to 2025 by [@IanVS](https://github.com/IanVS) in [#4037](https://github.com/wailsapp/wails/pull/4037)
99107
- Add docs for event.Sender by [@IanVS](https://github.com/IanVS) in [#4075](https://github.com/wailsapp/wails/pull/4075)
100108
- Go 1.24 support by [@leaanthony](https://github.com/leaanthony)
109+
- `ServiceStartup` hooks are now invoked when `App.Run` is called, not in `application.New` by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
110+
- `ServiceStartup` errors are now returned from `App.Run` instead of terminating the process by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
111+
- Binding and dialog calls from JS now reject with error objects instead of strings by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
101112

102113
## v3.0.0-alpha.9 - 2025-01-13
103114

docs/src/content/docs/learn/bindings.mdx

+146
Original file line numberDiff line numberDiff line change
@@ -448,3 +448,149 @@ const promise = MyService.LongRunningTask("input");
448448
// This will cause the context to be cancelled in the Go method
449449
promise.cancel();
450450
```
451+
452+
### Handling errors
453+
454+
As you may have noticed above, bound methods can return errors, which are handled specially.
455+
When a result field has type `error`, it is omitted by default from the values returned to JS.
456+
When such a field is _non-nil_, the promise rejects with a `RuntimeError` exception
457+
that wraps the Go error message:
458+
459+
```go
460+
func (*MyService) FailingMethod(name string) error {
461+
return fmt.Errorf("Welcome to an imperfect world, %s", name)
462+
}
463+
```
464+
465+
```js
466+
import { MyService } from './bindings/changeme';
467+
468+
try {
469+
await MyService.FailingMethod("CLU")
470+
} catch (err) {
471+
if (err.name === 'RuntimeError') {
472+
console.log(err.message); // Prints 'Welcome to an imperfect world, CLU'
473+
}
474+
}
475+
```
476+
477+
The exception will be an instance of the `Call.RuntimeError` class from the wails runtime,
478+
hence you can also test its type like this:
479+
480+
```js
481+
import { Call } from '@wailsio/runtime';
482+
483+
try {
484+
// ...
485+
} catch (err) {
486+
if (err instanceof Call.RuntimeError) {
487+
// ...
488+
}
489+
}
490+
```
491+
492+
If the Go error value supports JSON marshaling, the exception's `cause` property
493+
will hold the marshaled version of the error:
494+
495+
```go
496+
type ImperfectWorldError struct {
497+
Name string `json:"name"`
498+
}
499+
500+
func (err *ImperfectWorldError) Error() {
501+
return fmt.Sprintf("Welcome to an imperfect world, %s", err.Name)
502+
}
503+
504+
func (*MyService) FailingMethod(name string) error {
505+
return &ImperfectWorldError{
506+
Name: name,
507+
}
508+
}
509+
```
510+
511+
```js
512+
import { MyService } from './bindings/changeme';
513+
514+
try {
515+
await MyService.FailingMethod("CLU")
516+
} catch (err) {
517+
if (err.name === 'RuntimeError') {
518+
console.log(err.cause.name); // Prints 'CLU'
519+
}
520+
}
521+
```
522+
523+
Generally, many Go error values will only have limited or no support for marshaling to JSON.
524+
If you so wish, you can customise the value provided as cause
525+
by specifying either a global or per-service error marshaling function:
526+
527+
```go
528+
app := application.New(application.Options{
529+
MarshalError: func(err error) []byte {
530+
// ...
531+
},
532+
Services: []application.Service{
533+
application.NewServiceWithOptions(&MyService{}, application.ServiceOptions{
534+
MarshalError: func(err error) []byte {
535+
// ...
536+
},
537+
}),
538+
},
539+
})
540+
```
541+
542+
Per-service functions override the global function,
543+
which in turn overrides the default behaviour of using `json.Marshal`.
544+
If a marshaling function returns `nil`, it falls back to the outer function:
545+
per-service functions fall back to the global function,
546+
which in turn falls back to the default behaviour.
547+
548+
:::tip
549+
If you wish to omit the `cause` property on the resulting exception,
550+
let the marshaling function return a falsy JSON value like `[]byte("null")`.
551+
:::
552+
553+
Here's an example marshaling function that unwraps path errors and reports the file path:
554+
555+
```go
556+
app := application.New(application.Options{
557+
MarshalError: func(err error) []byte {
558+
var perr *fs.PathError
559+
if !errors.As(err, &perr) {
560+
// Not a path error, fall back to default handling.
561+
return nil
562+
}
563+
564+
// Marshal path string
565+
path, err := json.Marshal(&perr.Path)
566+
if err != nil {
567+
// String marshaling failed, fall back to default handling.
568+
return nil
569+
}
570+
571+
return []byte(fmt.Sprintf(`{"path":%s}`, path))
572+
},
573+
})
574+
```
575+
576+
:::note
577+
Error marshaling functions are not allowed to fail.
578+
If they are not able to process a given error and return valid JSON,
579+
they should return `nil` and fall back to a more generic handler.
580+
If no strategy succeeds, the exception will not have a `cause` property.
581+
:::
582+
583+
Binding call promises may also reject with a `TypeError`
584+
when the method has been passed the wrong number of arguments,
585+
when the conversion of arguments from JSON to their Go types fails,
586+
or when the conversion of results to JSON fails.
587+
These problems will usually be caught early by the type system.
588+
If your code typechecks but you still get type errors,
589+
it might be that some of your Go types are not supported by the `encoding/json` package:
590+
look for warnings from the binding generator to catch these.
591+
592+
:::caution
593+
If you see a `ReferenceError` complaining about unknown methods,
594+
it could mean that your JS bindings have gotten out of sync with Go code
595+
and must be regenerated.
596+
:::

docs/src/content/docs/learn/services.mdx

+31-5
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ greeting.
4040
## Registering a Service
4141

4242
To register a service with the application, you need to provide an instance of
43-
the service to the `Services` field of the `application.Options` struct (All
44-
services need to be wrapped by an `application.NewService` call. Here's an
45-
example:
43+
the service to the `Services` field of the `application.Options` struct.
44+
All services need to be wrapped by an `application.NewService` call.
45+
Here's an example:
4646

4747
```go
4848
app := application.New(application.Options{
@@ -70,6 +70,25 @@ ServiceOptions has the following fields:
7070
- Name - Specify a custom name for the Service
7171
- Route - A route to bind the Service to the frontend (more on this below)
7272

73+
After the application has been created but not yet started,
74+
you can register more services using the `RegisterService` method.
75+
This is useful when you need to feed a service some value
76+
that is only available after the application has been created.
77+
For example, let's wire application's logger into your own service:
78+
79+
```go
80+
app := application.New(application.Options{})
81+
82+
app.RegisterService(application.NewService(NewMyService(app.Logger)))
83+
84+
// ...
85+
86+
err := app.Run()
87+
```
88+
89+
Services may only be registered before running the application:
90+
`RegisterService` will panic if called after the `Run` method.
91+
7392
## Optional Methods
7493

7594
Services can implement optional methods to hook into the application lifecycle.
@@ -98,8 +117,12 @@ func (s *Service) ServiceStartup(ctx context.Context, options application.Servic
98117

99118
This method is called when the application is starting up. You can use it to
100119
initialize resources, set up connections, or perform any necessary setup tasks.
101-
The context is the application context, and the `options` parameter provides
102-
additional information about the service.
120+
The context is the application context that will be canceled upon shutdown,
121+
and the `options` parameter provides additional information about the service.
122+
123+
Services are initialised in the exact order of registration:
124+
first those listed in the `Services` field of the `application.Options` struct,
125+
then those added through the `RegisterService` method.
103126

104127
### ServiceShutdown
105128

@@ -110,6 +133,9 @@ func (s *Service) ServiceShutdown() error
110133
This method is called when the application is shutting down. Use it to clean up
111134
resources, close connections, or perform any necessary cleanup tasks.
112135

136+
Services are shut down in reverse registration order.
137+
The application context will be canceled before `ServiceShutdown` is called.
138+
113139
### ServeHTTP
114140

115141
```go

0 commit comments

Comments
 (0)