refactor(errors): normalize user-facing error messages
This commit is contained in:
+4
-1
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user