Improved panic handling. Added guide.

This commit is contained in:
Lea Anthony 2025-01-16 22:08:18 +11:00
commit e668c81806
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
23 changed files with 398 additions and 53 deletions

View file

@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add diagnostics section to `wails doctor` by [@leaanthony](https://github.com/leaanthony)
- Add window to context when calling a service method by [@leaanthony](https://github.com/leaanthony)
- Add `window-call` example to demonstrate how to know which window is calling a service by [@leaanthony](https://github.com/leaanthony)
- Better panic handling by [@leaanthony](https://github.com/leaanthony)
### Fixed
- Fixed Windows+Linux Edit Menu issues by [@leaanthony](https://github.com/leaanthony) in [#3f78a3a](https://github.com/wailsapp/wails/commit/3f78a3a8ce7837e8b32242c8edbbed431c68c062)

View file

@ -0,0 +1,111 @@
---
title: Handling Panics
description: How to handle panics in your Wails application
---
In Go applications, panics can occur during runtime when something unexpected happens. This guide explains how to handle panics both in general Go code and specifically in your Wails application.
## Understanding Panics in Go
Before diving into Wails-specific panic handling, it's essential to understand how panics work in Go:
1. Panics are for unrecoverable errors that shouldn't happen during normal operation
2. When a panic occurs in a goroutine, only that goroutine is affected
3. Panics can be recovered using `defer` and `recover()`
Here's a basic example of panic handling in Go:
```go
func doSomething() {
// Deferred functions run even when a panic occurs
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
}
}()
// Your code that might panic
panic("something went wrong")
}
```
For more detailed information about panic and recover in Go, see the [Go Blog: Defer, Panic, and Recover](https://go.dev/blog/defer-panic-and-recover).
## Panic Handling in Wails
Wails automatically handles panics that occur in your Service methods when they are called from the frontend. This means you don't need to add panic recovery to these methods - Wails will catch the panic and process it through your configured panic handler.
The panic handler is specifically designed to catch:
- Panics in bound service methods called from the frontend
- Internal panics from the Wails runtime
For other scenarios, such as background goroutines or standalone Go code, you should handle panics yourself using Go's standard panic recovery mechanisms.
## The PanicDetails Struct
When a panic occurs, Wails captures important information about the panic in a `PanicDetails` struct:
```go
type PanicDetails struct {
StackTrace string // The stack trace of where the panic occurred. Potentially trimmed to provide more context
Error error // The error that caused the panic
Time time.Time // The time when the panic occurred
FullStackTrace string // The complete stack trace including runtime frames
}
```
This structure provides comprehensive information about the panic:
- `StackTrace`: A formatted string showing the call stack that led to the panic
- `Error`: The actual error or panic message
- `Time`: The exact time when the panic occurred
- `FullStackTrace`: The complete stack trace including runtime frames
:::note[Panics in Service Code]
When panics are caught in your Service code after being called from the frontend, the stack trace is trimmed to focus on exactly where in your code the panic occurred.
If you want to see the full stack trace, you can use the `FullStackTrace` field.
:::
## Default Panic Handler
If you don't specify a custom panic handler, Wails will use its default handler which outputs error information in a formatted log message. For example:
```
Jan 16 21:18:05.649 ERR panic error: oh no! something went wrong deep in my service! :(
main.(*WindowService).call2
at E:/wails/v3/examples/panic-handling/main.go:24
main.(*WindowService).call1
at E:/wails/v3/examples/panic-handling/main.go:20
main.(*WindowService).GeneratePanic
at E:/wails/v3/examples/panic-handling/main.go:16
```
## Custom Panic Handler
You can implement your own panic handler by setting the `PanicHandler` option when creating your application. Here's an example:
```go
app := application.New(application.Options{
Name: "My App",
PanicHandler: func(panicDetails *application.PanicDetails) {
fmt.Printf("*** Custom Panic Handler ***\n")
fmt.Printf("Time: %s\n", panicDetails.Time)
fmt.Printf("Error: %s\n", panicDetails.Error)
fmt.Printf("Stacktrace: %s\n", panicDetails.StackTrace)
fmt.Printf("Full Stacktrace: %s\n", panicDetails.FullStackTrace)
// You could also:
// - Log to a file
// - Send to a crash reporting service
// - Show a user-friendly error dialog
// - Attempt to recover or restart the application
},
})
```
For a complete working example of panic handling in a Wails application, see the panic-handling example in `v3/examples/panic-handling`.
## Final Notes
Remember that the Wails panic handler is specifically for managing panics in bound methods and internal runtime errors. For other parts of your application, you should use Go's standard error handling patterns and panic recovery mechanisms where appropriate. As with all Go applications, it's better to prevent panics through proper error handling where possible.

View file

@ -0,0 +1,11 @@
# Panic Handling Example
This example is a demonstration of how to handle panics in your application.
## Running the example
To run the example, simply run the following command:
```bash
go run .
```

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Window Call Demo</title>
<style>
body {
color: white;
}
</style>
<script src="/wails/runtime.js" type="module"></script>
<script>
async function callBinding(name, ...params) {
return wails.Call.ByName(name, ...params)
}
</script>
</head>
<body>
<button onclick="panic()">Create a panic</button>
<script>
async function panic() {
await callBinding('main.WindowService.GeneratePanic')
}
</script>
</body>
</html>

View file

@ -0,0 +1,60 @@
package main
import (
"embed"
"fmt"
"github.com/wailsapp/wails/v3/pkg/application"
"log"
)
//go:embed assets/*
var assets embed.FS
type WindowService struct{}
func (s *WindowService) GeneratePanic() {
s.call1()
}
func (s *WindowService) call1() {
s.call2()
}
func (s *WindowService) call2() {
panic("oh no! something went wrong deep in my service! :(")
}
// ==============================================
func main() {
app := application.New(application.Options{
Name: "Panic Handler Demo",
Description: "A demo of Handling Panics",
Assets: application.AssetOptions{
Handler: application.BundledAssetFileServer(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
Services: []application.Service{
application.NewService(&WindowService{}),
},
PanicHandler: func(panicDetails *application.PanicDetails) {
fmt.Printf("*** There was a panic! ***\n")
fmt.Printf("Time: %s\n", panicDetails.Time)
fmt.Printf("Error: %s\n", panicDetails.Error)
fmt.Printf("Stacktrace: %s\n", panicDetails.StackTrace)
},
})
app.NewWebviewWindow().
SetTitle("WebviewWindow 1").
Show()
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View file

@ -232,17 +232,6 @@ type (
}
)
func processPanicHandlerRecover() {
h := globalApplication.options.PanicHandler
if h == nil {
return
}
if err := recover(); err != nil {
h(err)
}
}
// Messages sent from javascript get routed here
type windowMessage struct {
windowId uint
@ -486,7 +475,10 @@ func (a *App) OnApplicationEvent(eventType events.ApplicationEventType, callback
}
a.applicationEventListeners[eventID] = append(a.applicationEventListeners[eventID], listener)
if a.impl != nil {
go a.impl.on(eventID)
go func() {
defer handlePanic()
a.impl.on(eventID)
}()
}
return func() {
@ -537,13 +529,19 @@ func (a *App) GetPID() int {
func (a *App) info(message string, args ...any) {
if a.Logger != nil {
go a.Logger.Info(message, args...)
go func() {
defer handlePanic()
a.Logger.Info(message, args...)
}()
}
}
func (a *App) debug(message string, args ...any) {
if a.Logger != nil {
go a.Logger.Debug(message, args...)
go func() {
defer handlePanic()
a.Logger.Debug(message, args...)
}()
}
}
@ -593,9 +591,6 @@ func (a *App) NewSystemTray() *SystemTray {
func (a *App) Run() error {
// Setup panic handler
defer processPanicHandlerRecover()
// Call post-create hooks
err := a.preRun()
if err != nil {
@ -651,7 +646,10 @@ func (a *App) Run() error {
a.running = true
for _, systray := range a.pendingRun {
go systray.Run()
go func() {
defer handlePanic()
systray.Run()
}()
}
a.pendingRun = nil
@ -687,6 +685,7 @@ func (a *App) Run() error {
}
func (a *App) handleApplicationEvent(event *ApplicationEvent) {
defer handlePanic()
a.applicationEventListenersLock.RLock()
listeners, ok := a.applicationEventListeners[event.Id]
a.applicationEventListenersLock.RUnlock()
@ -708,11 +707,15 @@ func (a *App) handleApplicationEvent(event *ApplicationEvent) {
}
for _, listener := range listeners {
go listener.callback(event)
go func() {
defer handlePanic()
listener.callback(event)
}()
}
}
func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) {
defer handlePanic()
// Get window from window map
a.windowsLock.Lock()
window, ok := a.windows[event.windowId]
@ -726,6 +729,7 @@ func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) {
}
func (a *App) handleWindowMessage(event *windowMessage) {
defer handlePanic()
// Get window from window map
a.windowsLock.RLock()
window, ok := a.windows[event.windowId]
@ -745,10 +749,12 @@ func (a *App) handleWindowMessage(event *windowMessage) {
}
func (a *App) handleWebViewRequest(request *webViewAssetRequest) {
defer handlePanic()
a.assets.ServeWebViewRequest(request)
}
func (a *App) handleWindowEvent(event *windowEvent) {
defer handlePanic()
// Get window from window map
a.windowsLock.RLock()
window, ok := a.windows[event.WindowID]
@ -761,6 +767,8 @@ func (a *App) handleWindowEvent(event *windowEvent) {
}
func (a *App) handleMenuItemClicked(menuItemID uint) {
defer handlePanic()
menuItem := getMenuItemByID(menuItemID)
if menuItem == nil {
log.Printf("MenuItem #%d not found", menuItemID)
@ -1022,6 +1030,7 @@ func (a *App) removeKeyBinding(acceleratorString string) {
}
func (a *App) handleWindowKeyEvent(event *windowKeyEvent) {
defer handlePanic()
// Get window from window map
a.windowsLock.RLock()
window, ok := a.windows[event.windowId]

View file

@ -138,6 +138,7 @@ func (a *linuxApp) isDarkMode() bool {
func (a *linuxApp) monitorThemeChanges() {
go func() {
defer handlePanic()
conn, err := dbus.ConnectSessionBus()
if err != nil {
a.parent.info("[WARNING] Failed to connect to session bus; monitoring for theme changes will not function:", err)

View file

@ -84,7 +84,7 @@ type Options struct {
Flags map[string]any
// PanicHandler is called when a panic occurs
PanicHandler func(any)
PanicHandler func(*PanicDetails)
// DisableDefaultSignalHandler disables the default signal handler
DisableDefaultSignalHandler bool

View file

@ -280,24 +280,7 @@ var errorType = reflect.TypeFor[error]()
// Call will attempt to call this bound method with the given args
func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnValue interface{}, err error) {
// Use a defer statement to capture panics
defer func() {
if r := recover(); r != nil {
if str, ok := r.(string); ok {
if strings.HasPrefix(str, "reflect: Call using") {
// Remove prefix
str = strings.Replace(str, "reflect: Call using ", "", 1)
// Split on "as"
parts := strings.Split(str, " as type ")
if len(parts) == 2 {
err = fmt.Errorf("invalid argument type: got '%s', expected '%s'", parts[0], parts[1])
return
}
}
}
err = fmt.Errorf("%v", r)
}
}()
defer handlePanic(handlePanicOptions{skipEnd: 5})
argCount := len(args)
if b.needsContext {
argCount++
@ -309,7 +292,6 @@ func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnV
}
// Convert inputs to values of appropriate type
callArgs := make([]reflect.Value, argCount)
base := 0

View file

@ -35,7 +35,10 @@ func (m *linuxDialog) show() {
if response >= 0 && response < len(m.dialog.Buttons) {
button := m.dialog.Buttons[response]
if button.Callback != nil {
go button.Callback()
go func() {
defer handlePanic()
button.Callback()
}()
}
}
})

View file

@ -152,6 +152,7 @@ func (m *windowOpenFileDialog) show() (chan string, error) {
files := make(chan string)
go func() {
defer handlePanic()
for _, file := range result {
files <- file
}
@ -196,6 +197,7 @@ func (m *windowSaveFileDialog) show() (chan string, error) {
return cfd.NewSaveFileDialog(config)
}, false)
go func() {
defer handlePanic()
files <- result.(string)
close(files)
}()

View file

@ -131,8 +131,14 @@ func (e *EventProcessor) Emit(thisEvent *CustomEvent) {
}
}
go e.dispatchEventToListeners(thisEvent)
go e.dispatchEventToWindows(thisEvent)
go func() {
defer handlePanic()
e.dispatchEventToListeners(thisEvent)
}()
go func() {
defer handlePanic()
e.dispatchEventToWindows(thisEvent)
}()
}
func (e *EventProcessor) Off(eventName string) {
@ -219,7 +225,10 @@ func (e *EventProcessor) dispatchEventToListeners(event *CustomEvent) {
if listener.counter > 0 {
listener.counter--
}
go listener.callback(event)
go func() {
defer handlePanic()
listener.callback(event)
}()
if listener.counter == 0 {
listener.delete = true

View file

@ -1712,6 +1712,7 @@ func runChooserDialog(window pointer, allowMultiple, createFolders, showHidden b
InvokeAsync(func() {
response := C.gtk_dialog_run((*C.GtkDialog)(fc))
go func() {
defer handlePanic()
if response == C.GTK_RESPONSE_ACCEPT {
filenames := C.gtk_file_chooser_get_filenames((*C.GtkFileChooser)(fc))
iter := filenames

View file

@ -24,7 +24,7 @@ func InvokeSync(fn func()) {
var wg sync.WaitGroup
wg.Add(1)
globalApplication.dispatchOnMainThread(func() {
defer processPanicHandlerRecover()
defer handlePanic()
fn()
wg.Done()
})
@ -35,7 +35,7 @@ func InvokeSyncWithResult[T any](fn func() T) (res T) {
var wg sync.WaitGroup
wg.Add(1)
globalApplication.dispatchOnMainThread(func() {
defer processPanicHandlerRecover()
defer handlePanic()
res = fn()
wg.Done()
})
@ -47,7 +47,7 @@ func InvokeSyncWithError(fn func() error) (err error) {
var wg sync.WaitGroup
wg.Add(1)
globalApplication.dispatchOnMainThread(func() {
defer processPanicHandlerRecover()
defer handlePanic()
err = fn()
wg.Done()
})
@ -59,7 +59,7 @@ func InvokeSyncWithResultAndError[T any](fn func() (T, error)) (res T, err error
var wg sync.WaitGroup
wg.Add(1)
globalApplication.dispatchOnMainThread(func() {
defer processPanicHandlerRecover()
defer handlePanic()
res, err = fn()
wg.Done()
})
@ -71,7 +71,7 @@ func InvokeSyncWithResultAndOther[T any, U any](fn func() (T, U)) (res T, other
var wg sync.WaitGroup
wg.Add(1)
globalApplication.dispatchOnMainThread(func() {
defer processPanicHandlerRecover()
defer handlePanic()
res, other = fn()
wg.Done()
})
@ -81,7 +81,7 @@ func InvokeSyncWithResultAndOther[T any, U any](fn func() (T, U)) (res T, other
func InvokeAsync(fn func()) {
globalApplication.dispatchOnMainThread(func() {
defer processPanicHandlerRecover()
defer handlePanic()
fn()
})
}

View file

@ -262,7 +262,10 @@ func (m *MenuItem) handleClick() {
}
}
if m.callback != nil {
go m.callback(ctx)
go func() {
defer handlePanic()
m.callback(ctx)
}()
}
}

View file

@ -83,6 +83,11 @@ func (m *MessageProcessor) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
}
func (m *MessageProcessor) HandleRuntimeCallWithIDs(rw http.ResponseWriter, r *http.Request) {
defer func() {
if handlePanic() {
rw.WriteHeader(http.StatusInternalServerError)
}
}()
object, err := strconv.Atoi(r.URL.Query().Get("object"))
if err != nil {
m.httpError(rw, "Error decoding object value: "+err.Error())

View file

@ -104,6 +104,7 @@ func (m *MessageProcessor) processCallMethod(method int, rw http.ResponseWriter,
}
go func() {
defer handlePanic()
defer func() {
cancel()
@ -114,7 +115,8 @@ func (m *MessageProcessor) processCallMethod(method int, rw http.ResponseWriter,
result, err := boundMethod.Call(ctx, options.Args)
if err != nil {
m.callErrorCallback(window, "Error calling method: %s", callID, err)
msg := fmt.Sprintf("Error calling method '%v'", boundMethod.Name)
m.callErrorCallback(window, msg+": %s", callID, err)
return
}
var jsonResult = []byte("{}")

View file

@ -107,6 +107,7 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite
dialog := OpenFileDialogWithOptions(&options)
go func() {
defer handlePanic()
if options.AllowsMultipleSelection {
files, err := dialog.PromptForMultipleSelection()
if err != nil {
@ -148,6 +149,7 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite
dialog := SaveFileDialogWithOptions(&options)
go func() {
defer handlePanic()
file, err := dialog.PromptForSingleSelection()
if err != nil {
m.dialogErrorCallback(window, "Error getting selection: %s", dialogID, err)

View file

@ -0,0 +1,107 @@
package application
import (
"fmt"
"runtime"
"runtime/debug"
"strings"
"time"
)
func getStackTrace(skipStart int, skipEnd int) string {
// Get all program counters first
pc := make([]uintptr, 32)
n := runtime.Callers(skipStart+1, pc)
if n == 0 {
return ""
}
pc = pc[:n]
frames := runtime.CallersFrames(pc)
// Collect all frames first
var allFrames []runtime.Frame
for {
frame, more := frames.Next()
allFrames = append(allFrames, frame)
if !more {
break
}
}
// Remove frames from the end
if len(allFrames) > skipEnd {
allFrames = allFrames[:len(allFrames)-skipEnd]
}
// Build the output string
var builder strings.Builder
for _, frame := range allFrames {
fmt.Fprintf(&builder, "%s\n\tat %s:%d\n",
frame.Function, frame.File, frame.Line)
}
return builder.String()
}
type handlePanicOptions struct {
skipEnd int
}
type PanicDetails struct {
StackTrace string
Error error
Time time.Time
FullStackTrace string
}
func newPanicDetails(err error, trace string) *PanicDetails {
return &PanicDetails{
Error: err,
Time: time.Now(),
StackTrace: trace,
FullStackTrace: string(debug.Stack()),
}
}
// handlePanic handles any panics
// Returns the error if there was one
func handlePanic(options ...handlePanicOptions) bool {
// Try to recover
e := recover()
if e == nil {
return false
}
// Get the error
var err error
if errPanic, ok := e.(error); ok {
err = errPanic
} else {
err = fmt.Errorf("%v", e)
}
// Get the stack trace
var stackTrace string
skipEnd := 0
if len(options) > 0 {
skipEnd = options[0].skipEnd
}
stackTrace = getStackTrace(3, skipEnd)
processPanic(newPanicDetails(err, stackTrace))
return false
}
func processPanic(panicDetails *PanicDetails) {
h := globalApplication.options.PanicHandler
if h != nil {
h(panicDetails)
return
}
defaultPanicHandler(panicDetails)
}
func defaultPanicHandler(panicDetails *PanicDetails) {
errorMessage := fmt.Sprintf("panic error: %s\n%s", panicDetails.Error.Error(), panicDetails.StackTrace)
globalApplication.error(errorMessage)
}

View file

@ -74,6 +74,7 @@ func newSingleInstanceManager(app *App, options *SingleInstanceOptions) (*single
// Launch second instance data listener
once.Do(func() {
go func() {
defer handlePanic()
for encryptedData := range secondInstanceBuffer {
var secondInstanceData SecondInstanceData
var jsonData []byte

View file

@ -109,6 +109,7 @@ func (s *SystemTray) Run() {
}
s.attachedWindow.justClosed = true
go func() {
defer handlePanic()
time.Sleep(s.attachedWindow.Debounce)
s.attachedWindow.justClosed = false
}()

View file

@ -331,6 +331,7 @@ func (s *linuxSystemTray) run() {
}
s.setLabel(s.label)
go func() {
defer handlePanic()
s.register()
if err := conn.AddMatchSignal(

View file

@ -793,7 +793,10 @@ func (w *WebviewWindow) HandleWindowEvent(id uint) {
}
for _, listener := range w.eventListeners[id] {
go listener.callback(thisEvent)
go func() {
defer handlePanic()
listener.callback(thisEvent)
}()
}
w.dispatchWindowEvent(id)
}
@ -1261,7 +1264,10 @@ func (w *WebviewWindow) processKeyBinding(acceleratorString string) bool {
defer w.keyBindingsLock.RUnlock()
if callback := w.keyBindings[acceleratorString]; callback != nil {
// Execute callback
go callback(w)
go func() {
defer handlePanic()
callback(w)
}()
return true
}
}