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 <jason@rasm.me>

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
Min Idzelis 2026-01-21 11:46:08 -05:00 committed by GitHub
parent b669714bda
commit 280f906e4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 65 additions and 42 deletions

View file

@ -1009,9 +1009,11 @@
"error_getting_places": "Error getting places", "error_getting_places": "Error getting places",
"error_loading_image": "Error loading image", "error_loading_image": "Error loading image",
"error_loading_partners": "Error loading partners: {error}", "error_loading_partners": "Error loading partners: {error}",
"error_retrieving_asset_information": "Error retrieving asset information",
"error_saving_image": "Error: {error}", "error_saving_image": "Error: {error}",
"error_tag_face_bounding_box": "Error tagging face - cannot get bounding box coordinates", "error_tag_face_bounding_box": "Error tagging face - cannot get bounding box coordinates",
"error_title": "Error - Something went wrong", "error_title": "Error - Something went wrong",
"error_while_navigating": "Error while navigating to asset",
"errors": { "errors": {
"cannot_navigate_next_asset": "Cannot navigate to the next asset", "cannot_navigate_next_asset": "Cannot navigate to the next asset",
"cannot_navigate_previous_asset": "Cannot navigate to previous asset", "cannot_navigate_previous_asset": "Cannot navigate to previous asset",

View file

@ -252,7 +252,7 @@
await handleStopSlideshow(); await handleStopSlideshow();
} }
} }
}); }, $t('error_while_navigating'));
}; };
const showEditor = () => { const showEditor = () => {

View file

@ -11,10 +11,12 @@
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions'; import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
import { navigateToAsset } from '$lib/utils/asset-utils'; import { navigateToAsset } from '$lib/utils/asset-utils';
import { handleErrorAsync } from '$lib/utils/handle-error';
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import { type AlbumResponseDto, type AssetResponseDto, type PersonResponseDto, getAssetInfo } from '@immich/sdk'; import { type AlbumResponseDto, type AssetResponseDto, type PersonResponseDto, getAssetInfo } from '@immich/sdk';
import { onDestroy, onMount, untrack } from 'svelte'; import { onDestroy, onMount, untrack } from 'svelte';
import { t } from 'svelte-i18n';
let { asset: viewingAsset, gridScrollTarget } = assetViewingStore; let { asset: viewingAsset, gridScrollTarget } = assetViewingStore;
@ -38,28 +40,27 @@
person, person,
}: Props = $props(); }: Props = $props();
const getNextAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => { const getAsset = (id: string) => {
const earlierTimelineAsset = await timelineManager.getEarlierAsset(currentAsset); return handleErrorAsync(
if (earlierTimelineAsset) { () => assetCacheManager.getAsset({ ...authManager.params, id }),
const asset = await assetCacheManager.getAsset({ ...authManager.params, id: earlierTimelineAsset.id }); $t('error_retrieving_asset_information'),
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 getPreviousAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => { 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); const laterTimelineAsset = await timelineManager.getLaterAsset(currentAsset);
if (laterTimelineAsset) { if (!laterTimelineAsset) {
const asset = await assetCacheManager.getAsset({ ...authManager.params, id: laterTimelineAsset.id }); return;
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;
} }
return getAsset(laterTimelineAsset.id);
}; };
let assetCursor = $state<AssetCursor>({ let assetCursor = $state<AssetCursor>({
@ -87,10 +88,12 @@
const handleRandom = async () => { const handleRandom = async () => {
const randomAsset = await timelineManager.getRandomAsset(); const randomAsset = await timelineManager.getRandomAsset();
if (randomAsset) { if (!randomAsset) {
return;
}
await navigate({ targetRoute: 'current', assetId: randomAsset.id }); await navigate({ targetRoute: 'current', assetId: randomAsset.id });
return { id: randomAsset.id }; return { id: randomAsset.id };
}
}; };
const handleClose = async (asset: { id: string }) => { const handleClose = async (asset: { id: string }) => {
@ -180,12 +183,14 @@
}; };
const handleUndoDelete = async (assets: TimelineAsset[]) => { const handleUndoDelete = async (assets: TimelineAsset[]) => {
timelineManager.upsertAssets(assets); timelineManager.upsertAssets(assets);
if (assets.length > 0) { if (assets.length === 0) {
return;
}
const restoredAsset = assets[0]; const restoredAsset = assets[0];
const asset = await getAssetInfo({ ...authManager.params, id: restoredAsset.id }); const asset = await getAssetInfo({ ...authManager.params, id: restoredAsset.id });
assetViewingStore.setAsset(asset); assetViewingStore.setAsset(asset);
await navigate({ targetRoute: 'current', assetId: restoredAsset.id }); await navigate({ targetRoute: 'current', assetId: restoredAsset.id });
}
}; };
const handleUpdateOrUpload = (asset: AssetResponseDto) => { const handleUpdateOrUpload = (asset: AssetResponseDto) => {

View file

@ -19,12 +19,17 @@ export function getServerErrorMessage(error: unknown) {
return data?.message || error.message; return data?.message || error.message;
} }
export function handleError(error: unknown, message: string) { export function standardizeError(error: unknown) {
if ((error as Error)?.name === 'AbortError') { 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; return;
} }
console.error(`[handleError]: ${message}`, error, (error as Error)?.stack); console.error(`[handleError]: ${standardizedError}`, error, standardizedError.stack);
try { try {
let serverMessage = getServerErrorMessage(error); 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)`; serverMessage = `${String(serverMessage).slice(0, 75)}\n(Immich Server Error)`;
} }
const errorMessage = serverMessage || message; const errorMessage = serverMessage || localizedMessage;
toastManager.danger(errorMessage); toastManager.danger(errorMessage);
return errorMessage; return errorMessage;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return message; return localizedMessage;
}
}
export async function handleErrorAsync<T>(fn: () => Promise<T>, localizedMessage: string): Promise<T | undefined> {
try {
return await fn();
} catch (error: unknown) {
handleError(error, localizedMessage);
return;
} }
} }

View file

@ -1,3 +1,5 @@
import { handleError } from '$lib/utils/handle-error';
/** /**
* Tracks the state of asynchronous invocations to handle race conditions and stale operations. * 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 * This class helps manage concurrent operations by tracking which invocations are active
@ -51,10 +53,12 @@ export class InvocationTracker {
return this.invocationsStarted !== this.invocationsEnded; return this.invocationsStarted !== this.invocationsEnded;
} }
async invoke<T>(invocable: () => Promise<T>) { async invoke<T>(invocable: () => Promise<T>, localizedMessage: string) {
const invocation = this.startInvocation(); const invocation = this.startInvocation();
try { try {
return await invocable(); return await invocable();
} catch (error: unknown) {
handleError(error, localizedMessage);
} finally { } finally {
invocation.endInvocation(); invocation.endInvocation();
} }

View file

@ -24,11 +24,11 @@ export interface boundingBox {
export const getBoundingBox = ( export const getBoundingBox = (
faces: Faces[], faces: Faces[],
zoom: ZoomImageWheelState, zoom: ZoomImageWheelState,
photoViewer: HTMLImageElement | null, photoViewer: HTMLImageElement | undefined,
): boundingBox[] => { ): boundingBox[] => {
const boxes: boundingBox[] = []; const boxes: boundingBox[] = [];
if (photoViewer === null) { if (!photoViewer) {
return boxes; return boxes;
} }
const clientHeight = photoViewer.clientHeight; const clientHeight = photoViewer.clientHeight;
@ -93,7 +93,7 @@ export const zoomImageToBase64 = async (
image = img; image = img;
} }
if (image === null) { if (!image) {
return null; return null;
} }
const { boundingBoxX1: x1, boundingBoxX2: x2, boundingBoxY1: y1, boundingBoxY2: y2, imageWidth, imageHeight } = face; const { boundingBoxX1: x1, boundingBoxX2: x2, boundingBoxY1: y1, boundingBoxY2: y2, imageWidth, imageHeight } = face;
@ -121,11 +121,9 @@ export const zoomImageToBase64 = async (
canvas.height = faceHeight; canvas.height = faceHeight;
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
if (context) { if (!context) {
context.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight);
return canvas.toDataURL();
} else {
return null; return null;
} }
context.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight);
return canvas.toDataURL();
}; };