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