fix(error): add trace to browser in dev mode

also make AppError a bit easier to use
This commit is contained in:
zyachel
2025-06-01 13:03:08 +00:00
committed by ngn
parent bde980536d
commit ac60bda3bd
7 changed files with 83 additions and 35 deletions

View File

@ -11,6 +11,7 @@ type Props = {
message: string;
statusCode?: number;
originalPath?: string;
stack?: string;
/** props specific to error boundary. */
misc?: {
subtext: string;
@ -19,7 +20,9 @@ type Props = {
};
};
const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
const isDev = process.env.NODE_ENV === 'development';
const ErrorInfo = ({ message, statusCode, misc, originalPath, stack }: Props) => {
const title = statusCode ? `${message} (${statusCode})` : message;
return (
<>
@ -39,6 +42,11 @@ const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
<use href='/svg/sadgnu.svg#sad-gnu'></use>
</svg>
<h1 className={`heading heading__primary ${styles.heading}`}>{title}</h1>
{Boolean(stack && isDev) && (
<pre className={styles.stack}>
<code>{stack}</code>
</pre>
)}
{misc ? (
<>
<p>{misc.subtext}</p>
@ -64,6 +72,13 @@ const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
.
</p>
)}
<p>
If you think this shouldn't happen,{' '}
<Link href='/contact'>
<a className='link'>let it be known</a>
</Link>
.
</p>
</Layout>
</>
);

View File

@ -1,3 +1,3 @@
import { AppError as AppErrorClass } from 'src/utils/helpers';
export type AppError = Omit<InstanceType<typeof AppErrorClass>, 'name'>;
export type AppError = Pick<InstanceType<typeof AppErrorClass>, 'message' | 'statusCode' | 'stack'>;

View File

@ -67,13 +67,13 @@ export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = asy
props: { data: { title: query, results: res }, error: null, originalPath },
};
} catch (error) {
const { message, statusCode } = getErrorProperties(error);
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
const err = getErrorProperties(error);
ctx.res.statusCode = err.statusCode;
ctx.res.statusMessage = err.message;
return {
props: {
error: { message, statusCode },
error: { message: err.message, statusCode: err.statusCode, stack: err.format() },
data: { title: query, results: null },
originalPath,
},

View File

@ -55,11 +55,17 @@ export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx =>
return { props: { data, error: null, originalPath } };
} catch (error) {
const { message, statusCode } = getErrorProperties(error);
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
const err = getErrorProperties(error);
ctx.res.statusCode = err.statusCode;
ctx.res.statusMessage = err.message;
return { props: { error: { message, statusCode }, data: null, originalPath } };
return {
props: {
error: { message: err.message, statusCode: err.statusCode, stack: err.format() },
data: null,
originalPath,
},
};
}
};

View File

@ -5,7 +5,7 @@ import ErrorInfo from 'src/components/error/ErrorInfo';
import Media from 'src/components/media/Media';
import { Basic, Cast, DidYouKnow, Info, MoreLikeThis, Reviews } from 'src/components/title';
import Title from 'src/interfaces/shared/title';
import { AppError } from 'src/interfaces/shared/error';
import type { AppError } from 'src/interfaces/shared/error';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import title from 'src/utils/fetchers/title';
import { getErrorProperties, getProxiedIMDbImgUrl } from 'src/utils/helpers';
@ -63,12 +63,18 @@ export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx =>
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
return { props: { data, error: null, originalPath } };
} catch (error) {
const { message, statusCode } = getErrorProperties(error);
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
} catch (e) {
const err = getErrorProperties(e);
ctx.res.statusCode = err.statusCode;
ctx.res.statusMessage = err.message;
return { props: { error: { message, statusCode }, data: null, originalPath } };
const error = {
message: err.message,
statusCode: err.statusCode,
stack: err.format(),
};
console.error(err);
return { props: { error, data: null, originalPath } };
}
};

View File

@ -8,7 +8,7 @@
display: grid;
justify-content: center;
justify-items: center;
gap: var(--spacer-1);
gap: var(--comp-whitespace);
@include helper.bp('bp-700') {
--doc-whitespace: var(--spacer-5);
@ -31,6 +31,19 @@
text-align: center;
}
.stack {
max-width: 90%;
max-height: 20rem;
padding: var(--spacer-3);
white-space: pre-wrap;
overflow: scroll;
user-select: all;
border-radius: var(--spacer-1);
background-color: var(--clr-bg-muted);
}
.button {
align-self: end;

View File

@ -74,12 +74,33 @@ export const getProxiedIMDbImgUrl = (url: string) => {
};
export const AppError = class extends Error {
constructor(message: string, public statusCode: number, errorOptions?: unknown) {
const saneErrorOptions = getErrorOptions(errorOptions);
super(message, saneErrorOptions);
constructor(message: string, public statusCode: number, cause?: unknown) {
const _cause = cause ? AppError.toError(cause) : undefined;
super(message, { cause: _cause });
Error.captureStackTrace(this, AppError);
if (process.env.NODE_ENV === 'development') console.error(this);
}
static toError(err: unknown) {
if (err instanceof Error) return err;
return new Error(`Unexpected: ${JSON.stringify(err)}`);
}
format() {
let str = '';
let cur: Error | null = this;
let depth = 0;
while (cur && depth <= 4) {
if (cur.stack) str += `${cur.stack}\n`;
else str += `${cur.name}: ${cur.message}\n`;
cur = cur.cause instanceof Error ? cur.cause : null;
if (cur) str += 'Caused by:\n';
depth++;
}
return str.trimEnd();
}
};
@ -110,19 +131,6 @@ export const isLocalStorageAvailable = () => {
}
};
const getErrorOptions = (error: unknown): ErrorOptions | undefined => {
if (!error || typeof error !== 'object') return undefined;
let cause: unknown;
// @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded
if ('cause' in error) cause = error.cause;
// @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded
else if ('stack' in error) cause = error.stack;
// @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded
return { cause };
};
export const getErrorProperties = (
error: unknown,
message = 'Something went very wrong',
@ -130,4 +138,4 @@ export const getErrorProperties = (
) => {
if (error instanceof AppError) return error;
return new AppError(message, statusCode, error);
};
};