Skip to content

Event System

Wails provides a unified event system for pub/sub communication. Emit events from anywhere, listen from anywhere—Go to JavaScript, JavaScript to Go, window to window—enabling decoupled architecture with typed events and lifecycle hooks.

Go (emit):

app.Event.Emit("user-logged-in", map[string]interface{}{
"userId": 123,
"name": "Alice",
})

JavaScript (listen):

import { Events } from '@wailsio/runtime'
Events.On("user-logged-in", (event) => {
console.log(`User ${event.data.name} logged in`)
})

That’s it! Cross-language pub/sub.

Your application-specific events:

// Emit from Go
app.Event.Emit("order-created", order)
app.Event.Emit("payment-processed", payment)
app.Event.Emit("notification", message)
// Listen in JavaScript
Events.On("order-created", handleOrder)
Events.On("payment-processed", handlePayment)
Events.On("notification", showNotification)

Built-in OS and application events:

import "github.com/wailsapp/wails/v3/pkg/events"
// Theme changes
app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
if e.Context().IsDarkMode() {
app.Logger.Info("Dark mode enabled")
}
})
// Application lifecycle
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) {
app.Logger.Info("Application started")
})

Window-specific events:

window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
app.Logger.Info("Window focused")
})
window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) {
app.Logger.Info("Window closing")
})

Basic emit:

app.Event.Emit("event-name", data)

With different data types:

// String
app.Event.Emit("message", "Hello")
// Number
app.Event.Emit("count", 42)
// Struct
app.Event.Emit("user", User{ID: 1, Name: "Alice"})
// Map
app.Event.Emit("config", map[string]interface{}{
"theme": "dark",
"fontSize": 14,
})
// Array
app.Event.Emit("items", []string{"a", "b", "c"})

To specific window:

window.EmitEvent("window-specific-event", data)
import { Events } from '@wailsio/runtime'
// Emit to Go
Events.Emit("button-clicked", { buttonId: "submit" })
// Emit to all windows
Events.Emit("broadcast-message", "Hello everyone")

Application events:

app.Event.On("custom-event", func(e *application.CustomEvent) {
data := e.Data
// Handle event
})

With type assertion:

app.Event.On("user-updated", func(e *application.CustomEvent) {
user := e.Data.(User)
app.Logger.Info("User updated", "name", user.Name)
})

Multiple handlers:

// All handlers will be called
app.Event.On("order-created", logOrder)
app.Event.On("order-created", sendEmail)
app.Event.On("order-created", updateInventory)

Basic listener:

import { Events } from '@wailsio/runtime'
Events.On("event-name", (event) => {
console.log("Event received:", event.data)
})

With cleanup:

const unsubscribe = Events.On("event-name", handleEvent)
// Later, stop listening
unsubscribe()

Multiple handlers:

Events.On("data-updated", updateUI)
Events.On("data-updated", saveToCache)
Events.On("data-updated", logChange)

One Time handlers:

Events.Once("data-updated", updateVariable)

Common events (cross-platform):

import "github.com/wailsapp/wails/v3/pkg/events"
// Application started
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) {
app.Logger.Info("App started")
})
// Theme changed
app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
isDark := e.Context().IsDarkMode()
app.Event.Emit("theme-changed", isDark)
})
// File opened
app.Event.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *application.ApplicationEvent) {
filePath := e.Context().OpenedFile()
openFile(filePath)
})

Platform-specific events:

// Application became active
app.Event.OnApplicationEvent(events.Mac.ApplicationDidBecomeActive, func(e *application.ApplicationEvent) {
app.Logger.Info("App became active")
})
// Application will terminate
app.Event.OnApplicationEvent(events.Mac.ApplicationWillTerminate, func(e *application.ApplicationEvent) {
cleanup()
})

Common window events:

// Window focus
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
app.Logger.Info("Window focused")
})
// Window blur
window.OnWindowEvent(events.Common.WindowBlur, func(e *application.WindowEvent) {
app.Logger.Info("Window blurred")
})
// Window closing
window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) {
if hasUnsavedChanges() {
e.Cancel() // Prevent close
}
})
// Window closed
window.OnWindowEvent(events.Common.WindowClosed, func(e *application.WindowEvent) {
cleanup()
})

Hooks run before standard listeners and can cancel events:

// Hook - runs first, can cancel
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
if hasUnsavedChanges() {
result := showConfirmdialog("Unsaved changes. Close anyway?")
if result != "yes" {
e.Cancel() // Prevent window close
}
}
})
// Standard listener - runs after hooks
window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) {
app.Logger.Info("Window closing")
})

Key differences:

FeatureHooksStandard Listeners
Execution orderFirst, in registration orderAfter hooks, no guaranteed order
BlockingSynchronous, blocks next hookAsynchronous, non-blocking
Can cancelYesNo (already propagated)
Use caseControl flow, validationLogging, side effects
// Publisher (service)
type OrderService struct {
app *application.Application
}
func (o *OrderService) CreateOrder(items []Item) (*Order, error) {
order := &Order{Items: items}
if err := o.saveOrder(order); err != nil {
return nil, err
}
// Publish event
o.app.Event.Emit("order-created", order)
return order, nil
}
// Subscribers
app.Event.On("order-created", func(e *application.CustomEvent) {
order := e.Data.(*Order)
sendConfirmationEmail(order)
})
app.Event.On("order-created", func(e *application.CustomEvent) {
order := e.Data.(*Order)
updateInventory(order)
})
app.Event.On("order-created", func(e *application.CustomEvent) {
order := e.Data.(*Order)
logOrder(order)
})
// Frontend requests data
Emit("get-user-data", { userId: 123 })
// Backend responds
app.Event.On("get-user-data", func(e *application.CustomEvent) {
data := e.Data.(map[string]interface{})
userId := int(data["userId"].(float64))
user := getUserFromDB(userId)
// Send response
app.Event.Emit("user-data-response", user)
})
// Frontend receives response
Events.On("user-data-response", (event) => {
const user = event.data
displayUser(user)
})

Note: For request/response, bindings are better. Use events for notifications.

// Broadcast to all windows
app.Event.Emit("global-notification", "System update available")
// Each window handles it
Events.On("global-notification", (event) => {
const message = event.data
showNotification(message)
})
type EventAggregator struct {
events []Event
mu sync.Mutex
}
func (ea *EventAggregator) Add(event Event) {
ea.mu.Lock()
defer ea.mu.Unlock()
ea.events = append(ea.events, event)
// Emit batch every 100 events
if len(ea.events) >= 100 {
app.Event.Emit("event-batch", ea.events)
ea.events = nil
}
}

Go:

package main
import (
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
type NotificationService struct {
app *application.Application
}
func (n *NotificationService) Notify(message string) {
// Emit to all windows
n.app.Event.Emit("notification", map[string]interface{}{
"message": message,
"timestamp": time.Now(),
})
}
func main() {
app := application.New(application.Options{
Name: "Event Demo",
})
notifService := &NotificationService{app: app}
// System events
app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
isDark := e.Context().IsDarkMode()
app.Event.Emit("theme-changed", isDark)
})
// Custom events from frontend
app.Event.On("user-action", func(e *application.CustomEvent) {
data := e.Data.(map[string]interface{})
action := data["action"].(string)
app.Logger.Info("User action", "action", action)
// Respond
notifService.Notify("Action completed: " + action)
})
// Window events
window := app.Window.New()
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
app.Event.Emit("window-focused", window.Name())
})
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
// Confirm before close
app.Event.Emit("confirm-close", nil)
e.Cancel() // Wait for confirmation
})
app.Run()
}

JavaScript:

import { Events } from '@wailsio/runtime'
// Listen for notifications
Events.On("notification", (event) => {
showNotification(event.data.message)
})
// Listen for theme changes
Events.On("theme-changed", (event) => {
const isDark = event.data
document.body.classList.toggle('dark', isDark)
})
// Listen for window focus
Events.On("window-focused", (event) => {
const windowName = event.data
console.log(`Window ${windowName} focused`)
})
// Handle close confirmation
Events.On("confirm-close", (event) => {
if (confirm("Close window?")) {
Emit("close-confirmed", true)
}
})
// Emit user actions
document.getElementById('button').addEventListener('click', () => {
Emit("user-action", { action: "button-clicked" })
})
  • Use events for notifications - One-way communication
  • Use bindings for requests - Two-way communication
  • Keep event names consistent - Use kebab-case
  • Document event data - What fields are included?
  • Unsubscribe when done - Prevent memory leaks
  • Use hooks for validation - Control event flow
  • Don’t use events for RPC - Use bindings instead
  • Don’t emit too frequently - Batch if needed
  • Don’t block in handlers - Keep them fast
  • Don’t forget to unsubscribe - Memory leaks
  • Don’t use events for large data - Use bindings
  • Don’t create event loops - A emits B, B emits A

Event Patterns

Common event patterns and best practices.

Learn More →


Questions? Ask in Discord or check the event examples.