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