From 280f906e4bc8d78f8fd42a3fcc185ced1841e2b3 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Wed, 21 Jan 2026 11:46:08 -0500 Subject: [PATCH] feat: handle-error minor improvments (#25288) * feat: handle-error minor improvments * review comments * Update web/src/lib/utils/handle-error.ts Co-authored-by: Jason Rasmussen --------- Co-authored-by: Alex Co-authored-by: Jason Rasmussen --- i18n/en.json | 2 + .../asset-viewer/asset-viewer.svelte | 2 +- .../timeline/TimelineAssetViewer.svelte | 59 ++++++++++--------- web/src/lib/utils/handle-error.ts | 24 ++++++-- web/src/lib/utils/invocationTracker.ts | 6 +- web/src/lib/utils/people-utils.ts | 14 ++--- 6 files changed, 65 insertions(+), 42 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 0f0b450fd6..a2da7b783b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1009,9 +1009,11 @@ "error_getting_places": "Error getting places", "error_loading_image": "Error loading image", "error_loading_partners": "Error loading partners: {error}", + "error_retrieving_asset_information": "Error retrieving asset information", "error_saving_image": "Error: {error}", "error_tag_face_bounding_box": "Error tagging face - cannot get bounding box coordinates", "error_title": "Error - Something went wrong", + "error_while_navigating": "Error while navigating to asset", "errors": { "cannot_navigate_next_asset": "Cannot navigate to the next asset", "cannot_navigate_previous_asset": "Cannot navigate to previous asset", diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index f047b5001a..ac9c07df94 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -252,7 +252,7 @@ await handleStopSlideshow(); } } - }); + }, $t('error_while_navigating')); }; const showEditor = () => { diff --git a/web/src/lib/components/timeline/TimelineAssetViewer.svelte b/web/src/lib/components/timeline/TimelineAssetViewer.svelte index 06ff61d180..f61d88c1c4 100644 --- a/web/src/lib/components/timeline/TimelineAssetViewer.svelte +++ b/web/src/lib/components/timeline/TimelineAssetViewer.svelte @@ -11,10 +11,12 @@ import { handlePromiseError } from '$lib/utils'; import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions'; import { navigateToAsset } from '$lib/utils/asset-utils'; + import { handleErrorAsync } from '$lib/utils/handle-error'; import { navigate } from '$lib/utils/navigation'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { type AlbumResponseDto, type AssetResponseDto, type PersonResponseDto, getAssetInfo } from '@immich/sdk'; import { onDestroy, onMount, untrack } from 'svelte'; + import { t } from 'svelte-i18n'; let { asset: viewingAsset, gridScrollTarget } = assetViewingStore; @@ -38,28 +40,27 @@ person, }: Props = $props(); - const getNextAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => { - const earlierTimelineAsset = await timelineManager.getEarlierAsset(currentAsset); - if (earlierTimelineAsset) { - const asset = await assetCacheManager.getAsset({ ...authManager.params, id: earlierTimelineAsset.id }); - if (preload) { - // also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete - void getNextAsset(asset, false); - } - return asset; - } + const getAsset = (id: string) => { + return handleErrorAsync( + () => assetCacheManager.getAsset({ ...authManager.params, id }), + $t('error_retrieving_asset_information'), + ); }; - const getPreviousAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => { - const laterTimelineAsset = await timelineManager.getLaterAsset(currentAsset); - if (laterTimelineAsset) { - const asset = await assetCacheManager.getAsset({ ...authManager.params, id: laterTimelineAsset.id }); - if (preload) { - // also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete - void getPreviousAsset(asset, false); - } - return asset; + const getNextAsset = async (currentAsset: AssetResponseDto) => { + const earlierTimelineAsset = await timelineManager.getEarlierAsset(currentAsset); + if (!earlierTimelineAsset) { + return; } + return getAsset(earlierTimelineAsset.id); + }; + + const getPreviousAsset = async (currentAsset: AssetResponseDto) => { + const laterTimelineAsset = await timelineManager.getLaterAsset(currentAsset); + if (!laterTimelineAsset) { + return; + } + return getAsset(laterTimelineAsset.id); }; let assetCursor = $state({ @@ -87,10 +88,12 @@ const handleRandom = async () => { const randomAsset = await timelineManager.getRandomAsset(); - if (randomAsset) { - await navigate({ targetRoute: 'current', assetId: randomAsset.id }); - return { id: randomAsset.id }; + if (!randomAsset) { + return; } + + await navigate({ targetRoute: 'current', assetId: randomAsset.id }); + return { id: randomAsset.id }; }; const handleClose = async (asset: { id: string }) => { @@ -180,12 +183,14 @@ }; const handleUndoDelete = async (assets: TimelineAsset[]) => { timelineManager.upsertAssets(assets); - if (assets.length > 0) { - const restoredAsset = assets[0]; - const asset = await getAssetInfo({ ...authManager.params, id: restoredAsset.id }); - assetViewingStore.setAsset(asset); - await navigate({ targetRoute: 'current', assetId: restoredAsset.id }); + if (assets.length === 0) { + return; } + + const restoredAsset = assets[0]; + const asset = await getAssetInfo({ ...authManager.params, id: restoredAsset.id }); + assetViewingStore.setAsset(asset); + await navigate({ targetRoute: 'current', assetId: restoredAsset.id }); }; const handleUpdateOrUpload = (asset: AssetResponseDto) => { diff --git a/web/src/lib/utils/handle-error.ts b/web/src/lib/utils/handle-error.ts index 6db28123c2..4ff35510c6 100644 --- a/web/src/lib/utils/handle-error.ts +++ b/web/src/lib/utils/handle-error.ts @@ -19,12 +19,17 @@ export function getServerErrorMessage(error: unknown) { return data?.message || error.message; } -export function handleError(error: unknown, message: string) { - if ((error as Error)?.name === 'AbortError') { +export function standardizeError(error: unknown) { + return error instanceof Error ? error : new Error(String(error)); +} + +export function handleError(error: unknown, localizedMessage: string) { + const standardizedError = standardizeError(error); + if (standardizedError.name === 'AbortError') { return; } - console.error(`[handleError]: ${message}`, error, (error as Error)?.stack); + console.error(`[handleError]: ${standardizedError}`, error, standardizedError.stack); try { let serverMessage = getServerErrorMessage(error); @@ -32,13 +37,22 @@ export function handleError(error: unknown, message: string) { serverMessage = `${String(serverMessage).slice(0, 75)}\n(Immich Server Error)`; } - const errorMessage = serverMessage || message; + const errorMessage = serverMessage || localizedMessage; toastManager.danger(errorMessage); return errorMessage; } catch (error) { console.error(error); - return message; + return localizedMessage; + } +} + +export async function handleErrorAsync(fn: () => Promise, localizedMessage: string): Promise { + try { + return await fn(); + } catch (error: unknown) { + handleError(error, localizedMessage); + return; } } diff --git a/web/src/lib/utils/invocationTracker.ts b/web/src/lib/utils/invocationTracker.ts index 7d42d8c613..88c395a4ad 100644 --- a/web/src/lib/utils/invocationTracker.ts +++ b/web/src/lib/utils/invocationTracker.ts @@ -1,3 +1,5 @@ +import { handleError } from '$lib/utils/handle-error'; + /** * Tracks the state of asynchronous invocations to handle race conditions and stale operations. * This class helps manage concurrent operations by tracking which invocations are active @@ -51,10 +53,12 @@ export class InvocationTracker { return this.invocationsStarted !== this.invocationsEnded; } - async invoke(invocable: () => Promise) { + async invoke(invocable: () => Promise, localizedMessage: string) { const invocation = this.startInvocation(); try { return await invocable(); + } catch (error: unknown) { + handleError(error, localizedMessage); } finally { invocation.endInvocation(); } diff --git a/web/src/lib/utils/people-utils.ts b/web/src/lib/utils/people-utils.ts index 5fb03842b8..28425b948f 100644 --- a/web/src/lib/utils/people-utils.ts +++ b/web/src/lib/utils/people-utils.ts @@ -24,11 +24,11 @@ export interface boundingBox { export const getBoundingBox = ( faces: Faces[], zoom: ZoomImageWheelState, - photoViewer: HTMLImageElement | null, + photoViewer: HTMLImageElement | undefined, ): boundingBox[] => { const boxes: boundingBox[] = []; - if (photoViewer === null) { + if (!photoViewer) { return boxes; } const clientHeight = photoViewer.clientHeight; @@ -93,7 +93,7 @@ export const zoomImageToBase64 = async ( image = img; } - if (image === null) { + if (!image) { return null; } const { boundingBoxX1: x1, boundingBoxX2: x2, boundingBoxY1: y1, boundingBoxY2: y2, imageWidth, imageHeight } = face; @@ -121,11 +121,9 @@ export const zoomImageToBase64 = async ( canvas.height = faceHeight; const context = canvas.getContext('2d'); - if (context) { - context.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight); - - return canvas.toDataURL(); - } else { + if (!context) { return null; } + context.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight); + return canvas.toDataURL(); };