Skip to content

Every app fails. The disk fills up, the network disappears, the keyring is locked, the compositor restarts, a portal is missing, and a file is corrupted. Reliability is not the absence of failure; it is the ability to fail visibly, recover gracefully, and explain what happened.

This page is about the unglamorous engineering that decides whether users trust the app after the first bad day.

Failure What the user sees What the app should do
Disk full Save fails Keep the draft in memory, offer export, explain
Permission denied File cannot open Explain which permission; suggest the portal
Network down Sync fails Keep local data, retry with backoff, show offline state
Keyring locked Secret unavailable Prompt to unlock; do not crash
Portal missing File dialog unavailable Fall back to a clear explanation, not a dead button
Corrupt config App fails to start Back up, reset to defaults, tell the user
Corrupt database Data unreadable Restore backup, run integrity check, preserve the file
Compositor restart Window disappears/reappears A Wayland compositor restart normally kills the client connection; persist state continuously and restore on the next launch
Suspend/resume Timers fire late Recompute state, reconnect, refresh
Out of memory App killed The kernel sends SIGKILL; use GMemoryMonitor warnings to drop caches and bound memory before exhaustion
Dependency missing Import/startup error Ship the runtime; fail with a clear message

Handle errors at the layer that can do something about them.

  • Infrastructure returns typed errors with context.
  • Application decides: retry, fall back, or surface.
  • UI presents a human message and a next step.
  • Logging records the technical detail with a correlation ID.

Do not swallow exceptions. Do not show a raw traceback as the primary message. Do not use a crash as control flow.

A user-facing error with technical detail
try:
self.repository.save(note)
except StorageFullError as error:
self.show_error(
title="Could not save the note",
body="The disk is full. Free some space, then try again.",
details=str(error),
retry=self.save_current_note,
)

The “Details” expansion can show the technical error; the primary message must be understandable.

Logs are for diagnosis, not for users. They should be structured, leveled, rotated, and free of secrets.

#define G_LOG_DOMAIN "notes"
g_message("Opened database at %s", path);
g_warning("Sync failed: %s", error->message);
g_debug("Loaded %u notes", count);

GLib logs go to stderr, which systemd captures in the journal when the app is launched from a unit or desktop session. Use g_log_structured() if you need key-value fields.

Structured-ish logging
import logging
log = logging.getLogger("notes")
log.info("opened database", extra={"path": path})
log.warning("sync failed", extra={"error": str(error)})

Configure one handler at startup. Do not call logging.basicConfig() in library code. Send logs to stderr; let the session capture them.

Q_DECLARE_LOGGING_CATEGORY(notes)
Q_LOGGING_CATEGORY(notes, "notes")
qCInfo(notes) << "Opened database" << path;
qCWarning(notes) << "Sync failed" << error;

Use QT_LOGGING_RULES="notes.debug=true" to enable categories at runtime.

use tracing::{info, warn};
info!(path = %path.display(), "opened database");
warn!(error = %error, "sync failed");

Initialize tracing-subscriber once, with a level from RUST_LOG or a --debug flag. Do not log secrets or full document contents.

Level Use
Error The operation failed and the user needs to know
Warning Something is degraded but recoverable
Info Lifecycle events: start, open, save, sync
Debug Diagnostic detail, off by default
Trace Very verbose, development only

Rate-limit repeated warnings. A broken network should not produce one log line per retry per second.

On a systemd desktop, stderr is captured by the journal. Users and support can inspect it:

Terminal
journalctl --user -u notes.service -f
journalctl --user --since "1 hour ago" | grep notes

For a GUI app launched from the desktop, the journal may show the app’s stderr under the session. Use journalctl --user -b and filter by the app name or a structured field.

Do not write your own log rotation unless you have a specific need. If you do write a log file, put it in XDG_STATE_HOME, rotate it, and bound its size.

A GUI background service belongs in a user unit, not a hand-rolled daemon. Start from graphical-session.target, use PartOf=graphical-session.target, set StateDirectory=, and add basic hardening (NoNewPrivileges=true, PrivateTmp=true, ProtectSystem=strict). Test systemctl --user daemon-reload, enable --now, and journalctl --user -u.

Ubuntu uses systemd-coredump. Check for crashes:

Terminal
coredumpctl list notes
coredumpctl info notes
coredumpctl gdb notes

Ship with symbols or a separate debug package so the stack trace is useful. Do not strip everything and then ask users for a backtrace.

Remote reporting must be opt-in and privacy-preserving.

  • Show a crash dialog with “Send report” and “Don’t send”.
  • Explain what is included (stack trace, app version, OS, GPU) and what is not (documents, paths, secrets).
  • Scrub paths and usernames.
  • Use a project-specific DSN/key; do not embed a shared secret.
  • Provide a way to inspect the payload before sending.
  • Honor the user’s choice permanently.

Choose a crash backend by its data-retention policy, self-hosting option, SDK license, and redaction controls. The important part is consent and redaction, not the vendor name.

  • Write files atomically: temp file in the same directory, fsync, rename, then fsync the parent directory. The directory sync is what makes the rename durable after a power loss.
  • Use SQLite transactions for multi-step writes.
  • Enable WAL mode for concurrent reads; handle SQLITE_BUSY.
  • Version the schema and migrate forward in a transaction.
  • Back up before a destructive migration.
  • Run PRAGMA integrity_check on startup or on demand; if it fails, preserve the file and offer recovery.
  • Never truncate a file before the replacement is durable.
SQLite migration
with connection:
version = connection.execute("PRAGMA user_version").fetchone()[0]
if version < 2:
connection.execute("ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0")
connection.execute("PRAGMA user_version = 2")

Users should be able to recover from a bad state without losing everything.

  • Autosave drafts and restore them after a crash.
  • Session restore for open windows, tabs, and scroll positions.
  • Safe mode that starts with settings disabled when a plugin or config causes a crash.
  • Reset settings without deleting user data.
  • Export data in an open format before a destructive operation.
  • Backup rotation for the database and config.

A “Reset” button that silently deletes documents is not recovery; it is data loss.

Give support a way to collect the facts:

Terminal
notes --version
notes --debug
notes --doctor

--doctor can check:

  • app version, runtime, toolkit version;
  • session type (Wayland/X11) and desktop;
  • portal availability;
  • permissions (Flatpak/Snap);
  • config/data paths and writability;
  • database integrity;
  • recent errors (redacted).

A diagnostic bundle should be a single file the user can review before sending. Never include documents or secrets by default.

  • Cancel async work when a view closes.
  • Disconnect signal handlers when a widget is destroyed.
  • Release idle inhibitors, screen casts, and file locks.
  • Close database connections and flush writes on exit.
  • Remove temporary files and sockets.
  • Handle SIGTERM and session logout gracefully; do not block shutdown indefinitely.
  • ☐ Every I/O operation has an error path.
  • ☐ The app can start with a missing config, corrupt cache, and read-only data dir.
  • ☐ Writes are atomic; migrations are transactional.
  • ☐ Logs are leveled, rate-limited, and free of secrets.
  • ☐ Crashes produce a useful local backtrace.
  • ☐ Remote crash reporting is opt-in and redacted.
  • ☐ Autosave and session restore exist for editable content.
  • ☐ There is a safe mode or settings reset.
  • ☐ --version, --help, and --debug work.
  • ☐ The app persists state across suspend/resume and compositor restart, and handles OOM warnings without assuming it can catch SIGKILL.
  • ☐ Background work is cancellable and cleaned up.
  • ☐ Support has a diagnostic bundle path.

Next: AppStream & Metadata — how the software center sees your app.