refactor(errors): normalize user-facing error messages

This commit is contained in:
2026-07-03 13:05:46 +08:00
parent 65d972fbb4
commit 8a71041b5d
7 changed files with 72 additions and 10 deletions
+4 -1
View File
@@ -13,6 +13,7 @@
import { useEffect } from "react";
import Link from "next/link";
import { ExceptionHandler } from "@/core/errors";
import { ROUTES } from "@/router/routes";
import { Logger } from "@/utils";
@@ -29,6 +30,8 @@ export default function GlobalError({ error, unstable_retry }: ErrorProps) {
log.error("[app/error.tsx]", error);
}, [error]);
const errorMessage = ExceptionHandler.message(error, "Unexpected error");
return (
<div className="flex flex-1 flex-col items-center justify-center gap-md p-lg">
<h1
@@ -41,7 +44,7 @@ export default function GlobalError({ error, unstable_retry }: ErrorProps) {
className="max-w-md text-center text-sm"
style={{ color: "var(--color-text-secondary)" }}
>
{error.message || "Unexpected error"}
{errorMessage}
{error.digest ? ` (digest: ${error.digest})` : null}
</p>
<div className="flex gap-sm">
+8 -1
View File
@@ -11,12 +11,19 @@
* 因为它会替换根 layout。
*/
import { ExceptionHandler } from "@/core/errors";
interface GlobalErrorProps {
error: Error & { digest?: string };
unstable_retry?: () => void;
}
export default function GlobalError({ error }: GlobalErrorProps) {
const errorMessage = ExceptionHandler.message(
error,
"An unexpected error occurred.",
);
return (
<html lang="en">
<body
@@ -32,7 +39,7 @@ export default function GlobalError({ error }: GlobalErrorProps) {
Something went wrong
</h1>
<p style={{ marginBottom: "1rem", opacity: 0.8 }}>
{error.message || "An unexpected error occurred."}
{errorMessage}
</p>
<button
type="button"
@@ -13,6 +13,7 @@ import {
} from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import { ExceptionHandler } from "@/core/errors";
import { ROUTES } from "@/router/routes";
import { Logger } from "@/utils";
@@ -116,8 +117,10 @@ function StripePaymentForm({
declineCode: event.error.decline_code,
});
setErrorMessage(
event.error.message ??
ExceptionHandler.message(
event.error,
"Payment methods could not be loaded. Please try again later.",
),
);
};
@@ -130,7 +133,9 @@ function StripePaymentForm({
const { error: submitError } = await elements.submit();
if (submitError) {
setErrorMessage(submitError.message ?? "Please check your payment info.");
setErrorMessage(
ExceptionHandler.message(submitError, "Please check your payment info."),
);
setIsSubmitting(false);
return;
}
@@ -148,7 +153,9 @@ function StripePaymentForm({
});
if (error) {
setErrorMessage(error.message ?? "Payment confirmation failed.");
setErrorMessage(
ExceptionHandler.message(error, "Payment confirmation failed."),
);
setIsSubmitting(false);
return;
}
@@ -96,9 +96,23 @@ describe("ExceptionHandler", () => {
message: "plain failure",
});
expect(ExceptionHandler.handle({ message: "card declined" })).toEqual({
code: ExceptionCode.unknown,
message: "card declined",
});
expect(ExceptionHandler.handle({ nope: true })).toEqual({
code: ExceptionCode.unknown,
message: "Something went wrong. Please try again.",
});
});
it("uses call-site fallback text only for unrecognized errors", () => {
expect(ExceptionHandler.message({ nope: true }, "Domain fallback")).toBe(
"Domain fallback",
);
expect(ExceptionHandler.message(new Error("specific"), "Domain fallback")).toBe(
"specific",
);
});
});
+15 -3
View File
@@ -1,7 +1,11 @@
import { AppException } from "./app-exception";
import { ExceptionCode } from "./exception-code";
import type { ExceptionResult } from "./exception-result";
import { ApiExceptionHandler } from "./handlers/api-exception-handler";
import { FallbackExceptionHandler } from "./handlers/fallback-exception-handler";
import {
DEFAULT_EXCEPTION_MESSAGE,
FallbackExceptionHandler,
} from "./handlers/fallback-exception-handler";
import { NetworkExceptionHandler } from "./handlers/network-exception-handler";
import { PaymentExceptionHandler } from "./handlers/payment-exception-handler";
import { StorageExceptionHandler } from "./handlers/storage-exception-handler";
@@ -36,8 +40,16 @@ export class ExceptionHandler {
return FallbackExceptionHandler.handle(error);
}
static message(error: unknown): string {
return ExceptionHandler.handle(error).message;
static message(error: unknown, fallbackMessage?: string): string {
const result = ExceptionHandler.handle(error);
if (
fallbackMessage &&
result.code === ExceptionCode.unknown &&
result.message === DEFAULT_EXCEPTION_MESSAGE
) {
return fallbackMessage;
}
return result.message;
}
static toError(error: unknown): AppException {
@@ -1,6 +1,9 @@
import { ExceptionCode } from "../exception-code";
import type { ExceptionResult } from "../exception-result";
export const DEFAULT_EXCEPTION_MESSAGE =
"Something went wrong. Please try again.";
export class FallbackExceptionHandler {
static handle(error: unknown): ExceptionResult {
return {
@@ -19,5 +22,18 @@ function resolveMessage(error: unknown): string {
return error;
}
return "Something went wrong. Please try again.";
if (isMessageLike(error) && error.message.trim().length > 0) {
return error.message;
}
return DEFAULT_EXCEPTION_MESSAGE;
}
function isMessageLike(error: unknown): error is { message: string } {
return (
typeof error === "object" &&
error !== null &&
"message" in error &&
typeof error.message === "string"
);
}
+4 -1
View File
@@ -9,6 +9,7 @@
* - onError(errorMessage)
*/
import { getApiConfig } from "@/core/net/config/api_config";
import { ExceptionHandler } from "@/core/errors";
import { AppEnvUtil, Logger } from "@/utils";
const log = new Logger("ChatWebSocket");
@@ -89,7 +90,9 @@ export class ChatWebSocket {
};
} catch (e) {
logWebSocketError("✕ WS CONNECT FAILED", e);
this.onError?.(e instanceof Error ? e.message : String(e));
this.onError?.(
ExceptionHandler.message(e, "WebSocket connection failed."),
);
}
}