benbjohnson/litestream · error

set PERSIST_WAL: %w

Error message

set PERSIST_WAL: %w

What it means

This wraps any error produced by db.setPersistWAL during DB.init, including the connection acquisition, the FileControl type assertion, and the FileControlPersistWAL call. Litestream treats enabling PERSIST_WAL as mandatory because if SQLite deletes the WAL file when the last connection closes, replication falls out of sync. The wrapper is applied at the init call site so the failure context ('while initializing the database') is preserved.

Source

Thrown at db.go:1055

	}
	db.fileInfo = fi

	// Obtain permissions for parent directory.
	if fi, err = os.Stat(filepath.Dir(db.path)); err != nil {
		return err
	}
	db.dirInfo = fi

	dsn := fmt.Sprintf("file:%s?_pragma=busy_timeout(%d)&_pragma=wal_autocheckpoint(0)",
		db.path, db.BusyTimeout.Milliseconds())

	if db.db, err = sql.Open("sqlite", dsn); err != nil {
		return err
	}

	// Set PERSIST_WAL to prevent WAL file removal when database connections close.
	if err := db.setPersistWAL(ctx); err != nil {
		return fmt.Errorf("set PERSIST_WAL: %w", err)
	}

	// Open long-running database file descriptor. Required for non-OFD locks.
	if db.f, err = os.Open(db.path); err != nil {
		return fmt.Errorf("open db file descriptor: %w", err)
	}

	// Ensure database is closed if init fails.
	// Initialization can retry on next sync.
	defer func() {
		if err != nil {
			_ = db.releaseReadLock()
			db.db.Close()
			db.f.Close()
			db.db, db.f = nil, nil
		}
	}()

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Read the innermost wrapped error to determine which sub-step failed
  2. Upgrade modernc.org/sqlite and rebuild if the inner error is 'driver does not implement FileControl'
  3. URL-escape the database path / check the path for special characters that break the file: DSN (file:%s?_pragma=...)
  4. Ensure the database file exists and is writable by the litestream process before startup; retry init (litestream retries on next sync)
Defensive patterns

Strategy: try-catch

Validate before calling

if _, err := os.Stat(dbPath); err != nil { return err }
if err := checkWritable(dbPath); err != nil { return err }
// verify driver capability beforehand:
// go list -m modernc.org/sqlite

Try / catch

if err := db.init(ctx); err != nil {
    if strings.Contains(err.Error(), "set PERSIST_WAL") {
        log.Printf("init failed at PERSIST_WAL step: %v", err)
        // init retries on next sync; back off and retry
    }
}

Prevention

When it happens

Trigger: Calling DB.init (triggered by opening a database with litestream.Open or the first sync): sql.Open succeeds, then db.setPersistWAL(ctx) returns any error — get connection failure, 'driver does not implement FileControl', or 'FileControlPersistWAL: ...' — which is then re-wrapped as 'set PERSIST_WAL: %w'.

Common situations: App startup with an incompatible modernc.org/sqlite version (assertion failure); the SQLite database file disappearing or being locked between sql.Open and the connection; driver errors from an odd DSN built from the db path (spaces/special characters not URL-escaped in file: DSN).

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06). Data as JSON: /api/errors/acfc9222adcc2baa. Report an issue: GitHub.