import { type LoaderFunctionArgs } from '@remix-run/server-runtime';
import { Effect, pipe } from 'effect';
import { captureErrors, prettyPrint } from 'effect-errors';
import { getSpansDuration } from './logic/get-spans-duration';
import { remixThrow } from './logic/remix-throw';
export const effectLoader =
<A, E>(effect: (args: LoaderFunctionArgs) => Effect.Effect<A, E>) =>
async (args: LoaderFunctionArgs) =>
await Effect.runPromise(
pipe(
effect(args),
Effect.map((data) => ({ _tag: 'success' as const, data })),
Effect.sandbox,
Effect.catchAll((cause) => {
// Serverside logging
const errorsText = prettyPrint(cause, { stripCwd: true });
console.error(errorsText);
// Getting errors data to display it client side
const { errors } = captureErrors(cause, {
reverseSpans: true,
stripCwd: true,
});
// Computing spans duration ...
const errorsWithSpanDuration = errors.map(
({ errorType, message, stack, spans }) => ({
type: errorType,
message,
stack,
spans: getSpansDuration(spans),
}),
);
return Effect.succeed({
_tag: 'error' as const,
data: errorsWithSpanDuration,
});
}),
),
).then(remixThrow);
import { json } from '@remix-run/server-runtime';
import { match } from 'ts-pattern';
import {
EffectLoaderError,
EffectLoaderSuccess,
} from '../types/effect-loader.types';
import { stringifyErrorsMessage } from './stringify-errors-message';
type RemixThrowInput<A> = EffectLoaderSuccess<A> | EffectLoaderError;
const effectHasSucceeded = <A>(
p: RemixThrowInput<A>,
): p is EffectLoaderSuccess<A> => p._tag === 'success';
export const remixThrow = <A>(input: RemixThrowInput<A>) =>
match(input)
.when(
(p) => effectHasSucceeded(p),
({ data }) => data,
)
.otherwise(({ data }) => {
throw json(
{
type: 'effect',
errors: stringifyErrorsMessage(data as never),
},
{
status: 500,
},
);
});
export const loader = effectLoader(({ request }) =>
Effect.withSpan('my-route-loader', {
attributes: {
url: request.url,
method: request.method,
body: request.body,
},
})(
Effect.gen(function* () {
// My effect code ...
}),
),
);
import {
isRouteErrorResponse,
useLocation,
useRouteError,
} from '@remix-run/react';
export interface EffectError {
type?: string;
message: string;
stack?: string;
spans?: {
name: string;
attributes: Record<string, unknown>;
duration: bigint | undefined;
}[];
}
const isEffectError = (
error: unknown,
): error is {
data: {
type: 'effect';
errors: EffectError[];
};
} => (error as { data?: { type?: 'effect' } })?.data?.type === 'effect';
export const useErrorDetails = () => {
const { pathname } = useLocation();
const error = useRouteError();
if (isEffectError(error)) {
return {
_tag: 'effect' as const,
path: pathname,
errors: error.data.errors,
};
}
const isRoute = isRouteErrorResponse(error);
if (isRoute) {
return {
_tag: 'route' as const,
path: pathname,
errors: [
{
message: `${error.statusText}`,
},
],
};
}
if (error instanceof Error) {
return {
_tag: 'error' as const,
path: pathname,
errors: [error],
};
}
return {
_tag: 'unknown' as const,
path: pathname,
errors: [{ message: 'Unknown Error' }],
};
};
import { match } from 'ts-pattern';
import type { EffectError } from './useErrorDetails.code-sample';
import { useErrorDetails } from './useErrorDetails.code-sample';
type EffectErrorDetailsProps = Pick<EffectError, 'type' | 'message' | 'spans'>;
const EffectErrorDetails = ({
type,
message,
spans,
}: EffectErrorDetailsProps) => (
<li>
{type} {message}{' '}
{spans?.map(({ name, duration, attributes }, spanIndex) => (
<div key={spanIndex}>
<div>{name}</div>
<div>{duration !== undefined ? `~ ${duration} ms` : ''}</div>
<div>
{Object.entries(attributes)
.filter(([, value]) => value !== null)
.map(([key, value], attributeNumber) => (
<div key={attributeNumber}>
<span>{key}</span>: {JSON.stringify(value)}
</div>
))}
</div>
</div>
))}
</li>
);
const isEffectErrors = (
p: ReturnType<typeof useErrorDetails>,
): p is { _tag: 'effect'; path: string; errors: EffectError[] } =>
p._tag === 'effect';
export const ErrorBoundary = () => {
const errors = useErrorDetails();
return match(errors)
.when(isEffectErrors, ({ errors }) => (
<ul>
{errors.map((e, errorIndex) => (
<EffectErrorDetails key={errorIndex} {...e} />
))}
</ul>
))
.otherwise(({ errors }) => (
<ul>
{errors.map(({ message }, errorIndex) => (
<li key={errorIndex}>{message}</li>
))}
</ul>
));
};