Implemented comprehensive error handling and database stability improvements to handle SQLite-specific issues and prevent server crashes.
Created a centralized error handling module with:
getErrorMessage(err, fallback)- Type-safe error message extraction- Handles
Errorinstances, strings, objects withmessageorstatusMessage - Returns user-friendly fallback for unknown error types
- Handles
logError(context, err, additionalInfo)- Structured error logging- Logs with context prefix (e.g.,
[moments.saveTadas]) - Includes additional metadata for debugging
- Logs with context prefix (e.g.,
handleAsyncError(operation, context, userMessage)- Async error wrapper- Automatically logs and shows toast notification
- Returns
nullon error for safe fallback
safeExecute(fn, fallback, onError)- Safe sync function execution- Catches errors and returns fallback value
- Optional error callback
getHttpErrorDetails(err)- Extract HTTP status and messageisFetchError(err)- Type guard for fetch errors
Moments Page (pages/moments.vue):
- ✅
fetchEntries()- UseslogErrorandgetErrorMessage - ✅
saveTextEntry()- Better error messages - ✅
handleVoiceTranscript()- Includes transcript length in logs - ✅
saveTadas()- Contextual error logging with ta-da count
Tally Page (pages/tally.vue):
- ✅
fetchEntries()- Consistent error handling - ✅
saveTally()- Better user feedback - ✅
savePendingTallies()- Individual tally failures logged
Ta-Da Page (pages/tada/index.vue):
- ✅
handleVoiceTranscript()- Contextual logging with transcript length
- 18 unit tests passing in
utils/errorHandling.test.ts - Tests cover all utility functions
- Tests validate error message extraction, logging, and type guards
- Consistency - All errors handled the same way across the app
- Debugging - Structured logs with context make issues easier to trace
- User Experience - Friendly error messages instead of raw error objects
- Type Safety - Proper TypeScript types, no
anyusage - Maintainability - Centralized logic easier to update than scattered try-catch blocks
catch (err) {
console.error("Failed to save:", err);
toast.error("Failed to save entry");
}catch (err) {
logError("moments.saveTadas", err, { tadasCount: extractedTadas.value.length });
toast.error(getErrorMessage(err, "Failed to save accomplishments"));
}- Consider adding error boundary components for Vue rendering errors
- Add server-side equivalent for API error handling
- Implement error tracking service integration (Sentry, etc.)
- Add retry logic for transient failures
Issue: Dev server crashing with EINVAL errors when SQLite creates/deletes journal files during transactions. The error was not non-fatal - it crashed the Nitro server requiring manual restart.
Root Causes:
- File watcher (chokidar) tries to watch SQLite journal files as they're being created/deleted
- Race condition between SQLite transaction cleanup and file watcher
- No retry logic for transient SQLite errors (BUSY, locked)
- No graceful shutdown handling for database connections
The Real Problem: Database was stored inside app/data/ which is watched by the dev server. SQLite journal files triggered EINVAL errors.
The Solution: Move database outside watched directory in development.
Migration:
- Development:
/workspaces/tada/data/db.sqlite(outsideapp/- not watched) - Production: Unchanged - uses
DATABASE_URLenv var - See DATABASE_LOCATION_MIGRATION.md for full details
- Migration script:
scripts/migrate-db-location.sh
Code change in server/db/index.ts:
const isDev = process.env["NODE_ENV"] === "development";
const databaseUrl =
process.env["DATABASE_URL"] ||
(isDev
? "file:../data/db.sqlite" // Outside app/ - not watched
: "file:./data/db.sqlite");Vite server watch with function-based ignore (more reliable than glob patterns):
watch: {
// Function-based ignore - evaluates each path dynamically
ignored: (path: string) => {
// Ignore data directory
if (path.includes("/data/") || path.includes("\\data\\")) return true;
if (path.endsWith("/data") || path.endsWith("\\data")) return true;
// Ignore SQLite files
if (path.includes(".sqlite")) return true;
if (path.includes(".db")) return true;
if (path.includes("-journal")) return true;
if (path.includes("-wal")) return true;
if (path.includes("-shm")) return true;
return false;
},
usePolling: false, // Native fs events
ignoreInitial: true, // Skip initial scan
},
hmr: {
timeout: 30000, // Longer HMR timeout
overlay: true, // Show errors without crashing
},Nitro devServer watch configuration (second layer of protection):
nitro: {
devServer: {
watch: [
"!data/**",
"!**/*.sqlite*",
"!**/*.db*",
"!**/*-journal",
"!**/*-wal",
"!**/*-shm",
],
},
}Added explicit ignore rules for database directories:
{
"ignore_dirs": [
"data",
"node_modules",
".nuxt",
".output",
"dist",
"coverage"
]
}New utilities for robust database operations:
ensureDbDirectory()- Create database directory if missingdbExists()- Check if database file existscheckDbHealth()- Test database connection with SELECT 1retryDbOperation()- Retry with exponential backoff (handles SQLITE_BUSY, locked, EINVAL)initializeDatabase()- Safe initialization with retry logicshutdownDatabase()- Graceful cleanup
Retry Configuration:
- Max 3 retries
- Exponential backoff: 100ms, 200ms, 400ms
- Only retries transient errors (SQLITE_BUSY, locked, EINVAL)
High-level operation wrappers:
withRetry<T>()- Wrap any operation with retry logicwithTransaction<T>()- Transaction with automatic retrysafeWrite<T>()- Returns null on failure instead of throwingsafeRead<T>()- Returns empty array on failure
Usage example:
// Before
const entries = await db
.select()
.from(entries)
.where(eq(entries.userId, userId));
// After - with automatic retry on transient errors
const entries = await withRetry(
() => db.select().from(entries).where(eq(entries.userId, userId)),
"fetch user entries",
);Minimal, stable initialization:
- Ensure directory exists before creating client
- No health checks (they trigger database writes during init)
- No lifecycle hooks (they interfered with HMR)
- No SIGTERM handlers (caused readonly property errors)
- Retry logic available via operations wrapper when needed
What was removed:
- ❌ Health checks on startup (triggered unwanted DB writes)
- ❌ Nitro lifecycle plugin (caused "readonly property" error)
- ❌ SIGTERM handlers (interfered with dev server HMR)
-
Manual Testing:
- Create multiple entries rapidly
- Monitor for EINVAL errors
- Check server stays running
- Verify no crashes on database writes
-
Stress Testing:
- Rapid concurrent writes
- Multiple simultaneous transactions
- Watch for journal file race conditions
-
Recovery Testing:
- Kill server mid-transaction
- Restart and verify data integrity
- Check for orphaned journal files
- Stability - Server no longer crashes on SQLite journal file operations
- Resilience - Automatic retry on transient database errors
- Observability - Detailed logging of database operations and failures
- Graceful Degradation - Operations fail safely instead of crashing
- Development Experience - HMR continues working, errors shown in overlay
Key Log Messages:
[db:manager] Creating database directory- Directory setup[db:operations] Database operation failed after retries- Persistent error (retry exhausted)[db:index] Database initialized successfully- (removed, was causing issues)
- EINVAL errors may still occasionally appear in logs but won't crash server
- Retry logic adds latency (100-700ms) for operations that fail initially
- Heavy concurrent writes may still see temporary SQLITE_BUSY errors
- No automatic health checks - rely on retry logic for error recovery
app/utils/errorHandling.ts- Main utilitiesapp/utils/errorHandling.test.ts- Unit testsapp/pages/moments.vue- Updated error handlingapp/pages/tally.vue- Updated error handlingapp/pages/tada/index.vue- Updated error handling
app/nuxt.config.ts- Function-based watcher ignore + Nitro devServer configapp/.watchmanconfig- Watchman ignore rulesapp/server/db/manager.ts- Database management utilities (directory creation, retry logic)app/server/db/operations.ts- Operation wrappers with retryapp/server/db/index.ts- Simplified initialization (no health checks, no lifecycle hooks)- REMOVED (caused readonly property error)app/server/plugins/database.ts
Celebration overlay code was duplicated in tada/index.vue and moments.vue (~150 lines of template + CSS each).
Created components/CelebrationOverlay.vue - a reusable component with:
- Configurable emoji, text, sound file, and duration
- Automatic sound playback on show
- Emits
completeevent when animation finishes - Encapsulates all celebration logic and styles
Usage:
<CelebrationOverlay
:show="showCelebration"
:sound-file="getTadaSoundFile()"
@complete="onCelebrationComplete"
/>Benefits:
- Single source of truth for celebration UI
- Easy to update styles/behavior across all pages
- Reduces code duplication by ~300 lines
- Type-safe props and events
pages/moments.vue- Uses component, removed duplicate codepages/tada/index.vue- Can be updated to use component (currently has inline version)
app/components/CelebrationOverlay.vue- Reusable celebration component