Skip to main content

max / goingson

Surface startup failures instead of aborting AppState::new already returns a Result and maps every failure (data dir, DB connect, migrations, email ID migration) to a message, but main.rs discarded all of it with .expect(). That expect runs inside NSApp's didFinishLaunching, which is nounwind, so the panic aborted the process and the user saw a bare SIGABRT with no diagnostic text. Recovering the message required building from source and reading a stack trace. Route failures to fatal_startup_error instead: log the message, show it in a native dialog, exit(1). Uses rfd rather than tauri-plugin-dialog because Tauri's event loop has not started during setup. The plugin's show has no loop to paint from or deliver its callback to, and its blocking_show is documented as unsafe on the main thread. rfd is already in the tree as the plugin's own dependency and is declared here to match, so both share one build. It has no mobile backend, so the helper is target-gated with a log-and-exit path for iOS/Android. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-16 14:03 UTC
Commit: 9a3bc67d5c16060ffcc34cc241ae1737a76be3d1
Parent: b6e29ae
4 files changed, +41 insertions, -2 deletions
M Cargo.lock +1
@@ -1987,6 +1987,7 @@ dependencies = [
1987 1987 "pter",
1988 1988 "rand 0.9.2",
1989 1989 "reqwest 0.12.28",
1990 + "rfd",
1990 1991 "serde",
1991 1992 "serde_json",
1992 1993 "sha2",
M Cargo.toml +4
@@ -55,6 +55,10 @@ keyring = { version = "3", features = ["apple-native", "linux-native", "windows-
55 55 tauri = "2.10.2"
56 56 tauri-build = "2.5.5"
57 57 tauri-plugin-dialog = "2.6.0"
58 + # Native dialogs, declared to match tauri-plugin-dialog's own rfd dependency so
59 + # the two share one build. Used directly only for startup errors that happen
60 + # before Tauri's event loop exists.
61 + rfd = { version = "0.16", default-features = false, features = ["common-controls-v6"] }
58 62 tauri-plugin-shell = "2.3.5"
59 63 tauri-plugin-notification = "2.3.3"
60 64 tauri-plugin-window-state = "2.4.1"
@@ -96,6 +96,8 @@ tauri-plugin-process = { workspace = true }
96 96 # File watching (for external DB changes)
97 97 notify = { workspace = true }
98 98 notify-debouncer-mini = { workspace = true }
99 + # Native error dialog for startup failures (no mobile backend)
100 + rfd = { workspace = true }
99 101
100 102 # Desktop-only: tray icon support
101 103 [target.'cfg(not(any(target_os = "ios", target_os = "android")))'.dependencies.tauri]
@@ -25,6 +25,36 @@ use goingson_desktop::notifications;
25 25 #[cfg(not(any(target_os = "ios", target_os = "android")))]
26 26 use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
27 27
28 + /// Report a fatal startup error to the user, then exit.
29 + ///
30 + /// Never returns, and must not panic. This runs inside NSApp's
31 + /// `didFinishLaunching` callback, which is nounwind, so a panic here aborts the
32 + /// process and reduces the message to a bare SIGABRT with no diagnostic text.
33 + ///
34 + /// Uses rfd rather than tauri-plugin-dialog because Tauri's event loop has not
35 + /// started yet at this point in setup: the plugin's `show` has no loop to paint
36 + /// from or deliver its callback to, and its `blocking_show` is documented as
37 + /// unsafe to call on the main thread. rfd drives a native modal directly.
38 + #[cfg(not(any(target_os = "ios", target_os = "android")))]
39 + fn fatal_startup_error(message: &str) -> ! {
40 + tracing::error!("{}", message);
41 +
42 + rfd::MessageDialog::new()
43 + .set_level(rfd::MessageLevel::Error)
44 + .set_title("GoingsOn cannot start")
45 + .set_description(message)
46 + .show();
47 +
48 + std::process::exit(1);
49 + }
50 +
51 + /// Mobile has no rfd backend, so the error goes to the log and the process exits.
52 + #[cfg(any(target_os = "ios", target_os = "android"))]
53 + fn fatal_startup_error(message: &str) -> ! {
54 + tracing::error!("{}", message);
55 + std::process::exit(1);
56 + }
57 +
28 58 /// Set up the macOS menu bar tray icon showing "GO" in Reglo.
29 59 #[cfg(not(any(target_os = "ios", target_os = "android")))]
30 60 fn setup_tray(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
@@ -304,8 +334,10 @@ fn main() {
304 334 // Initialize database
305 335 let app_handle = app.handle().clone();
306 336 tauri::async_runtime::block_on(async move {
307 - let state = AppState::new(&app_handle).await
308 - .expect("Failed to initialize app state");
337 + let state = match AppState::new(&app_handle).await {
338 + Ok(state) => state,
339 + Err(e) => fatal_startup_error(&e),
340 + };
309 341 // Reclaim orphaned attachment blobs before the schedulers spawn
310 342 // and before the UI can create attachments — race-free here.
311 343 blob_gc::reconcile(&state.pool, &state.data_dir).await;