diff --git a/apps/mobile-app/app/(tabs)/items/index.tsx b/apps/mobile-app/app/(tabs)/items/index.tsx
index 2a02d2905..baffc3e66 100644
--- a/apps/mobile-app/app/(tabs)/items/index.tsx
+++ b/apps/mobile-app/app/(tabs)/items/index.tsx
@@ -338,14 +338,14 @@ export default function ItemsScreen(): React.ReactNode {
setIsLoadingItems(false);
} catch (err) {
console.error('Error loading items:', err);
- Toast.show({
- type: 'error',
- text1: t('items.errorLoadingItems'),
- text2: t('common.errors.unknownError'),
+ const errorMessage = err instanceof Error ? err.message : String(err);
+ const errorStack = err instanceof Error ? err.stack ?? '' : '';
+ router.replace({
+ pathname: '/vault-error',
+ params: { errorMessage, errorStack, errorSource: 'items' },
});
- setIsLoadingItems(false);
}
- }, [dbContext.sqliteClient, setIsLoadingItems, t]);
+ }, [dbContext.sqliteClient, setIsLoadingItems, router]);
useEffect(() => {
const unsubscribeFocus = navigation.addListener('focus', () => {
diff --git a/apps/mobile-app/app/_layout.tsx b/apps/mobile-app/app/_layout.tsx
index 93587c92f..49b2be943 100644
--- a/apps/mobile-app/app/_layout.tsx
+++ b/apps/mobile-app/app/_layout.tsx
@@ -145,6 +145,7 @@ function RootLayoutNav() : React.ReactNode {
+
diff --git a/apps/mobile-app/app/initialize.tsx b/apps/mobile-app/app/initialize.tsx
index a85f2ed97..b7bb52cd9 100644
--- a/apps/mobile-app/app/initialize.tsx
+++ b/apps/mobile-app/app/initialize.tsx
@@ -50,107 +50,122 @@ export default function Initialize() : React.ReactNode {
* Initialize the app.
*/
const initialize = async () : Promise => {
- const { isLoggedIn, enabledAuthMethods } = await app.initializeAuth();
+ try {
+ const { isLoggedIn, enabledAuthMethods } = await app.initializeAuth();
- if (!isLoggedIn) {
- router.replace('/login');
- return;
- }
-
- /**
- * If we already have an unlocked vault, we can skip the biometric unlock
- * but still need to perform vault sync to check for updates.
- */
- const isAlreadyUnlocked = await NativeVaultManager.isVaultUnlocked();
-
- if (!isAlreadyUnlocked) {
- // Check if we have an encrypted database and if FaceID is enabled
- try {
- const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
-
- if (hasEncryptedDatabase) {
- // Attempt automatic unlock using centralized helper
- updateStatus(t('app.status.unlockingVault'));
- const unlockResult = await VaultUnlockHelper.attemptAutomaticUnlock({ enabledAuthMethods, unlockVault: dbContext.unlockVault });
-
- if (!unlockResult.success) {
- /*
- * Unlock failed or cancelled, redirect to unlock screen.
- * Only log non-cancellation errors to avoid noise.
- */
- if (!unlockResult.error?.includes('cancelled')) {
- console.error('Automatic unlock failed:', unlockResult.error);
- }
- router.replace('/unlock');
- return;
- }
-
- // Check if the vault needs migration before syncing
- if (await dbContext.hasPendingMigrations()) {
- router.replace('/upgrade');
- return;
- }
- }
- } catch (err) {
- console.error('Error during initial vault unlock:', err);
- router.replace('/unlock');
+ if (!isLoggedIn) {
+ router.replace('/login');
return;
}
- } else {
+
/**
- * Vault already unlocked (e.g., from password unlock)
- * Check if migrations are needed.
+ * If we already have an unlocked vault, we can skip the biometric unlock
+ * but still need to perform vault sync to check for updates.
*/
- if (await dbContext.hasPendingMigrations()) {
- router.replace('/upgrade');
- return;
+ const isAlreadyUnlocked = await NativeVaultManager.isVaultUnlocked();
+
+ if (!isAlreadyUnlocked) {
+ // Check if we have an encrypted database and if FaceID is enabled
+ try {
+ const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
+
+ if (hasEncryptedDatabase) {
+ // Attempt automatic unlock using centralized helper
+ updateStatus(t('app.status.unlockingVault'));
+ const unlockResult = await VaultUnlockHelper.attemptAutomaticUnlock({ enabledAuthMethods, unlockVault: dbContext.unlockVault });
+
+ if (!unlockResult.success) {
+ /*
+ * Unlock failed or cancelled, redirect to unlock screen.
+ * Only log non-cancellation errors to avoid noise.
+ */
+ if (!unlockResult.error?.includes('cancelled')) {
+ console.error('Automatic unlock failed:', unlockResult.error);
+ }
+ router.replace('/unlock');
+ return;
+ }
+
+ // Check if the vault needs migration before syncing
+ if (await dbContext.hasPendingMigrations()) {
+ router.replace('/upgrade');
+ return;
+ }
+ }
+ } catch (err) {
+ console.error('Error during initial vault unlock:', err);
+ router.replace('/unlock');
+ return;
+ }
+ } else {
+ /**
+ * Vault already unlocked (e.g., from password unlock)
+ * Check if migrations are needed.
+ */
+ if (await dbContext.hasPendingMigrations()) {
+ router.replace('/upgrade');
+ return;
+ }
}
+
+ /*
+ * Perform vault sync in background - don't block app access.
+ * The ServerSyncIndicator will show sync progress/offline status.
+ * This also handles uploading pending local changes (isDirty) from previous sessions.
+ */
+ void (async (): Promise => {
+ try {
+ await syncVault({
+ /**
+ * Handle successful vault sync.
+ */
+ onSuccess: async () => {
+ // Sync completed - ServerSyncIndicator will update
+ await dbContext.refreshSyncState();
+ },
+ /**
+ * Handle offline state - just set offline mode and continue.
+ * The ServerSyncIndicator will show offline status.
+ */
+ onOffline: async () => {
+ await dbContext.setIsOffline(true);
+ await dbContext.refreshSyncState();
+ },
+ /**
+ * Handle error during vault sync.
+ */
+ onError: async (error: string) => {
+ console.error('Vault sync error during initialize:', error);
+ await dbContext.refreshSyncState();
+ },
+ /**
+ * On upgrade required.
+ */
+ onUpgradeRequired: () : void => {
+ router.replace('/upgrade');
+ },
+ });
+ } finally {
+ await dbContext.refreshSyncState();
+ }
+ })();
+
+ // Navigate immediately - don't wait for sync
+ navigation.navigateAfterUnlock();
+ } catch (err) {
+ /*
+ * Catch any unhandled errors during vault initialization to prevent hard crashes.
+ * Redirect to vault error screen where users can view and copy the error details
+ * for remote debugging purposes.
+ */
+ console.error('Fatal error during vault initialization:', err);
+ const errorMessage = err instanceof Error ? err.message : String(err);
+ const errorStack = err instanceof Error ? err.stack ?? '' : '';
+ router.replace({
+ pathname: '/vault-error',
+ params: { errorMessage, errorStack, errorSource: 'initialize' },
+ });
}
-
- /*
- * Perform vault sync in background - don't block app access.
- * The ServerSyncIndicator will show sync progress/offline status.
- * This also handles uploading pending local changes (isDirty) from previous sessions.
- */
- void (async (): Promise => {
- try {
- await syncVault({
- /**
- * Handle successful vault sync.
- */
- onSuccess: async () => {
- // Sync completed - ServerSyncIndicator will update
- await dbContext.refreshSyncState();
- },
- /**
- * Handle offline state - just set offline mode and continue.
- * The ServerSyncIndicator will show offline status.
- */
- onOffline: async () => {
- await dbContext.setIsOffline(true);
- await dbContext.refreshSyncState();
- },
- /**
- * Handle error during vault sync.
- */
- onError: async (error: string) => {
- console.error('Vault sync error during initialize:', error);
- await dbContext.refreshSyncState();
- },
- /**
- * On upgrade required.
- */
- onUpgradeRequired: () : void => {
- router.replace('/upgrade');
- },
- });
- } finally {
- await dbContext.refreshSyncState();
- }
- })();
-
- // Navigate immediately - don't wait for sync
- navigation.navigateAfterUnlock();
};
initialize();
diff --git a/apps/mobile-app/app/reinitialize.tsx b/apps/mobile-app/app/reinitialize.tsx
index 0e8b4eb8f..c71aaf997 100644
--- a/apps/mobile-app/app/reinitialize.tsx
+++ b/apps/mobile-app/app/reinitialize.tsx
@@ -48,115 +48,130 @@ export default function ReinitializeScreen() : React.ReactNode {
* Initialize the app.
*/
const initialize = async () : Promise => {
- const { isLoggedIn, enabledAuthMethods } = await app.initializeAuth();
+ try {
+ const { isLoggedIn, enabledAuthMethods } = await app.initializeAuth();
- // If user is not logged in, navigate to login immediately
- if (!isLoggedIn) {
- router.replace('/login');
- return;
- }
+ // If user is not logged in, navigate to login immediately
+ if (!isLoggedIn) {
+ router.replace('/login');
+ return;
+ }
- /**
- * If we already have an unlocked vault, we can skip the biometric unlock
- * but still need to perform vault sync to check for updates.
- */
- const isAlreadyUnlocked = await NativeVaultManager.isVaultUnlocked();
+ /**
+ * If we already have an unlocked vault, we can skip the biometric unlock
+ * but still need to perform vault sync to check for updates.
+ */
+ const isAlreadyUnlocked = await NativeVaultManager.isVaultUnlocked();
- if (!isAlreadyUnlocked) {
- // Check if we have an encrypted database
- try {
- const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
+ if (!isAlreadyUnlocked) {
+ // Check if we have an encrypted database
+ try {
+ const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
- if (hasEncryptedDatabase) {
- // Attempt automatic unlock using centralized helper
- updateStatus(t('app.status.unlockingVault'));
- // Small delay to ensure loading screen is fully rendered before showing native unlock dialog
- await new Promise(resolve => setTimeout(resolve, 150));
- const unlockResult = await VaultUnlockHelper.attemptAutomaticUnlock({ enabledAuthMethods, unlockVault: dbContext.unlockVault });
+ if (hasEncryptedDatabase) {
+ // Attempt automatic unlock using centralized helper
+ updateStatus(t('app.status.unlockingVault'));
+ // Small delay to ensure loading screen is fully rendered before showing native unlock dialog
+ await new Promise(resolve => setTimeout(resolve, 150));
+ const unlockResult = await VaultUnlockHelper.attemptAutomaticUnlock({ enabledAuthMethods, unlockVault: dbContext.unlockVault });
- if (!unlockResult.success) {
- // Unlock failed, redirect to unlock screen
- console.error('Automatic unlock failed:', unlockResult.error);
+ if (!unlockResult.success) {
+ // Unlock failed, redirect to unlock screen
+ console.error('Automatic unlock failed:', unlockResult.error);
+ router.replace('/unlock');
+ return;
+ }
+
+ // Add small delay for UX
+ await new Promise(resolve => setTimeout(resolve, 150));
+ updateStatus(t('app.status.decryptingVault'));
+ await new Promise(resolve => setTimeout(resolve, 300));
+
+ // Check if the vault needs migration before syncing
+ if (await dbContext.hasPendingMigrations()) {
+ router.replace('/upgrade');
+ return;
+ }
+ } else {
+ // No encrypted database, redirect to unlock screen
router.replace('/unlock');
return;
}
-
- // Add small delay for UX
- await new Promise(resolve => setTimeout(resolve, 150));
- updateStatus(t('app.status.decryptingVault'));
- await new Promise(resolve => setTimeout(resolve, 300));
-
- // Check if the vault needs migration before syncing
- if (await dbContext.hasPendingMigrations()) {
- router.replace('/upgrade');
- return;
- }
- } else {
- // No encrypted database, redirect to unlock screen
+ } catch (err) {
+ console.error('Error during initial vault unlock:', err);
router.replace('/unlock');
return;
}
- } catch (err) {
- console.error('Error during initial vault unlock:', err);
- router.replace('/unlock');
- return;
+ } else {
+ /**
+ * Vault already unlocked (e.g., from password unlock)
+ * Check if migrations are needed.
+ */
+ if (await dbContext.hasPendingMigrations()) {
+ router.replace('/upgrade');
+ return;
+ }
}
- } else {
- /**
- * Vault already unlocked (e.g., from password unlock)
- * Check if migrations are needed.
+
+ /*
+ * Perform vault sync in background - don't block app access.
+ * The ServerSyncIndicator will show sync progress/offline status.
+ * This also handles uploading pending local changes (isDirty) from previous sessions.
*/
- if (await dbContext.hasPendingMigrations()) {
- router.replace('/upgrade');
- return;
- }
+ void (async (): Promise => {
+ try {
+ await syncVault({
+ /**
+ * Handle successful vault sync.
+ */
+ onSuccess: async () => {
+ // Sync completed - ServerSyncIndicator will update
+ await dbContext.refreshSyncState();
+ },
+ /**
+ * Handle error during vault sync.
+ * Authentication errors are already handled in useVaultSync.
+ */
+ onError: async (error: string) => {
+ console.error('Vault sync error during reinitialize:', error);
+ await dbContext.refreshSyncState();
+ },
+ /**
+ * Handle offline state - just set offline mode and continue.
+ * The ServerSyncIndicator will show offline status.
+ */
+ onOffline: async () => {
+ await dbContext.setIsOffline(true);
+ await dbContext.refreshSyncState();
+ },
+ /**
+ * On upgrade required.
+ */
+ onUpgradeRequired: () : void => {
+ router.replace('/upgrade');
+ },
+ });
+ } finally {
+ await dbContext.refreshSyncState();
+ }
+ })();
+
+ // Navigate immediately
+ navigation.navigateAfterUnlock();
+ } catch (err) {
+ /*
+ * Catch any unhandled errors during vault re-initialization to prevent hard crashes.
+ * Redirect to vault error screen where users can view and copy the error details
+ * for remote debugging purposes.
+ */
+ console.error('Fatal error during vault re-initialization:', err);
+ const errorMessage = err instanceof Error ? err.message : String(err);
+ const errorStack = err instanceof Error ? err.stack ?? '' : '';
+ router.replace({
+ pathname: '/vault-error',
+ params: { errorMessage, errorStack, errorSource: 'reinitialize' },
+ });
}
-
- /*
- * Perform vault sync in background - don't block app access.
- * The ServerSyncIndicator will show sync progress/offline status.
- * This also handles uploading pending local changes (isDirty) from previous sessions.
- */
- void (async (): Promise => {
- try {
- await syncVault({
- /**
- * Handle successful vault sync.
- */
- onSuccess: async () => {
- // Sync completed - ServerSyncIndicator will update
- await dbContext.refreshSyncState();
- },
- /**
- * Handle error during vault sync.
- * Authentication errors are already handled in useVaultSync.
- */
- onError: async (error: string) => {
- console.error('Vault sync error during reinitialize:', error);
- await dbContext.refreshSyncState();
- },
- /**
- * Handle offline state - just set offline mode and continue.
- * The ServerSyncIndicator will show offline status.
- */
- onOffline: async () => {
- await dbContext.setIsOffline(true);
- await dbContext.refreshSyncState();
- },
- /**
- * On upgrade required.
- */
- onUpgradeRequired: () : void => {
- router.replace('/upgrade');
- },
- });
- } finally {
- await dbContext.refreshSyncState();
- }
- })();
-
- // Navigate immediately
- navigation.navigateAfterUnlock();
};
initialize();
diff --git a/apps/mobile-app/app/vault-error.tsx b/apps/mobile-app/app/vault-error.tsx
new file mode 100644
index 000000000..f6795f4b3
--- /dev/null
+++ b/apps/mobile-app/app/vault-error.tsx
@@ -0,0 +1,286 @@
+import { MaterialIcons } from '@expo/vector-icons';
+import { LinearGradient } from 'expo-linear-gradient';
+import { router, useLocalSearchParams } from 'expo-router';
+import { useState, useCallback } from 'react';
+import { StyleSheet, View, ScrollView, Dimensions, Text, Platform } from 'react-native';
+
+import { copyToClipboard } from '@/utils/ClipboardUtility';
+
+import { useColors } from '@/hooks/useColorScheme';
+import { useLogout } from '@/hooks/useLogout';
+import { useTranslation } from '@/hooks/useTranslation';
+
+import Logo from '@/assets/images/logo.svg';
+import { ThemedText } from '@/components/themed/ThemedText';
+import { ThemedView } from '@/components/themed/ThemedView';
+import { RobustPressable } from '@/components/ui/RobustPressable';
+
+/**
+ * Vault error screen displayed when the app encounters an unrecoverable error
+ * during vault initialization or bootstrap.
+ */
+export default function VaultErrorScreen() : React.ReactNode {
+ const { errorMessage, errorStack, errorSource } = useLocalSearchParams<{
+ errorMessage: string;
+ errorStack: string;
+ errorSource: string;
+ }>();
+ const colors = useColors();
+ const { t } = useTranslation();
+ const { logoutUserInitiated } = useLogout();
+ const [showDetails, setShowDetails] = useState(false);
+ const [copied, setCopied] = useState(false);
+
+ /**
+ * Copy the full error details to clipboard for sharing with support.
+ */
+ const handleCopyError = useCallback(async (): Promise => {
+ const errorReport = [
+ `Source: ${errorSource ?? 'unknown'}`,
+ `Platform: ${Platform.OS} ${Platform.Version}`,
+ `Error: ${errorMessage ?? 'Unknown error'}`,
+ '',
+ 'Stack trace:',
+ errorStack ?? 'No stack trace available',
+ ].join('\n');
+
+ await copyToClipboard(errorReport);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }, [errorMessage, errorStack, errorSource]);
+
+ /**
+ * Retry by navigating back to the initialize screen.
+ */
+ const handleRetry = useCallback((): void => {
+ router.replace('/initialize');
+ }, []);
+
+ const styles = StyleSheet.create({
+ appName: {
+ color: colors.text,
+ fontSize: 28,
+ fontWeight: 'bold',
+ textAlign: 'center',
+ },
+ button: {
+ alignItems: 'center',
+ backgroundColor: colors.primary,
+ borderRadius: 8,
+ justifyContent: 'center',
+ marginBottom: 12,
+ minHeight: 50,
+ paddingVertical: 8,
+ width: '100%',
+ },
+ buttonText: {
+ color: colors.primarySurfaceText,
+ fontSize: 16,
+ fontWeight: '600',
+ paddingHorizontal: 16,
+ paddingVertical: 4,
+ textAlign: 'center',
+ },
+ container: {
+ flex: 1,
+ },
+ content: {
+ backgroundColor: colors.accentBackground,
+ borderRadius: 10,
+ padding: 20,
+ width: '100%',
+ },
+ copyButton: {
+ alignItems: 'center',
+ backgroundColor: colors.accentBackground,
+ borderColor: colors.accentBorder,
+ borderRadius: 8,
+ borderWidth: 1,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginTop: 8,
+ minHeight: 40,
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ },
+ copyButtonText: {
+ color: colors.primary,
+ fontSize: 14,
+ fontWeight: '600',
+ marginLeft: 6,
+ },
+ detailsContainer: {
+ backgroundColor: colors.background,
+ borderColor: colors.accentBorder,
+ borderRadius: 8,
+ borderWidth: 1,
+ marginTop: 12,
+ maxHeight: 250,
+ padding: 12,
+ },
+ detailsText: {
+ color: colors.textMuted,
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ fontSize: 11,
+ },
+ errorIcon: {
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ errorMessageText: {
+ color: colors.errorText,
+ fontSize: 14,
+ textAlign: 'center',
+ },
+ gradientContainer: {
+ height: Dimensions.get('window').height * 0.4,
+ left: 0,
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ },
+ headerSection: {
+ paddingBottom: 24,
+ paddingHorizontal: 16,
+ paddingTop: 24,
+ },
+ logoContainer: {
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ logoutButton: {
+ alignSelf: 'center',
+ justifyContent: 'center',
+ marginTop: 8,
+ },
+ logoutButtonText: {
+ color: colors.red,
+ fontSize: 16,
+ },
+ mainContent: {
+ flex: 1,
+ justifyContent: 'center',
+ paddingBottom: 40,
+ paddingHorizontal: 20,
+ },
+ scrollContent: {
+ flexGrow: 1,
+ },
+ showDetailsButton: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginTop: 12,
+ },
+ showDetailsText: {
+ color: colors.primary,
+ fontSize: 14,
+ marginLeft: 4,
+ },
+ subtitle: {
+ color: colors.text,
+ fontSize: 14,
+ marginBottom: 16,
+ opacity: 0.7,
+ textAlign: 'center',
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+ {t('app.vaultError.title')}
+
+
+
+
+
+
+
+
+ {t('app.vaultError.description')}
+
+
+
+ {errorMessage ?? t('common.errors.unknownError')}
+
+
+ {/* Show/hide details toggle */}
+ setShowDetails(!showDetails)}
+ >
+
+
+ {showDetails ? t('app.vaultError.hideDetails') : t('app.vaultError.showDetails')}
+
+
+
+ {/* Stack trace details */}
+ {showDetails && (
+
+
+
+ {`Source: ${errorSource ?? 'unknown'}\nPlatform: ${Platform.OS} ${Platform.Version}\n\n${errorStack ?? 'No stack trace available'}`}
+
+
+
+
+
+ {copied ? t('common.copied') : t('app.vaultError.copyErrorDetails')}
+
+
+
+ )}
+
+ {/* Action buttons */}
+
+
+
+ {t('common.retry')}
+
+
+
+
+
+
+ {t('auth.logout')}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/mobile-app/i18n/locales/en.json b/apps/mobile-app/i18n/locales/en.json
index e194147ad..aede6f3c2 100644
--- a/apps/mobile-app/i18n/locales/en.json
+++ b/apps/mobile-app/i18n/locales/en.json
@@ -26,6 +26,7 @@
"generate": "Generate",
"attachments": "Attachments",
"or": "or",
+ "retry": "Retry",
"deleteItemConfirmTitle": "Delete Item",
"deleteItemConfirmDescription": "Are you sure you want to delete this item?",
"errors": {
@@ -618,6 +619,13 @@
"vaultAutoLockedMessage": "Vault auto-locked after timeout.",
"attemptingToUnlockMessage": "Attempting to unlock."
},
+ "vaultError": {
+ "title": "Vault Error",
+ "description": "An error occurred while loading your vault. You can retry or copy the error details to share with support.",
+ "showDetails": "Show error details",
+ "hideDetails": "Hide error details",
+ "copyErrorDetails": "Copy error details"
+ },
"loginSettings": {
"title": "API Connection",
"aliasvaultNet": "Aliasvault.net",
diff --git a/apps/mobile-app/utils/SqliteClient.ts b/apps/mobile-app/utils/SqliteClient.ts
index d12d580b4..1750e0b34 100644
--- a/apps/mobile-app/utils/SqliteClient.ts
+++ b/apps/mobile-app/utils/SqliteClient.ts
@@ -250,8 +250,12 @@ class SqliteClient implements IDatabaseClient {
const results = await NativeVaultManager.executeQuery(query, convertedParams);
return results as T[];
} catch (error) {
- console.error('Error executing query:', error);
- throw error;
+ const originalMessage = error instanceof Error ? error.message : String(error);
+ const queryPreview = query.trim().substring(0, 200);
+ const enrichedError = new Error(originalMessage);
+ enrichedError.stack = `SQL: ${queryPreview}\n\n${error instanceof Error && error.stack ? error.stack : ''}`;
+ console.error('Error executing query:', enrichedError.message);
+ throw enrichedError;
}
}
@@ -278,8 +282,12 @@ class SqliteClient implements IDatabaseClient {
const result = await NativeVaultManager.executeUpdate(query, convertedParams);
return result as number;
} catch (error) {
- console.error('Error executing update:', error);
- throw error;
+ const originalMessage = error instanceof Error ? error.message : String(error);
+ const queryPreview = query.trim().substring(0, 200);
+ const enrichedError = new Error(originalMessage);
+ enrichedError.stack = `SQL: ${queryPreview}\n\n${error instanceof Error && error.stack ? error.stack : ''}`;
+ console.error('Error executing update:', enrichedError.message);
+ throw enrichedError;
}
}