Skip to content

Instantly share code, notes, and snippets.

@dmmulroy
Created April 4, 2025 17:30
Show Gist options
  • Save dmmulroy/2fc6ce8e53005a30f9fa35b5747cdd11 to your computer and use it in GitHub Desktop.
Save dmmulroy/2fc6ce8e53005a30f9fa35b5747cdd11 to your computer and use it in GitHub Desktop.
import { Data } from 'effect';
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { type ResultAsync, errAsync, okAsync } from 'neverthrow';
export class ParseError extends Data.TaggedError('ParseError')<{
response: Response;
issues: ReadonlyArray<StandardSchemaV1.Issue>;
}> {}
export class UnexpectedError extends Data.TaggedError('UnexpectedError')<{
cause?: unknown;
}> {}
export class ApiError<E extends StandardSchemaV1> extends Data.TaggedError(
'ApiError',
)<{
error: SchemaOutput<E>;
}> {}
export type SafeFetchOptions<
Success extends StandardSchemaV1,
Failure extends StandardSchemaV1,
> = Readonly<{
successSchema: Success;
failureSchema: Failure;
}> &
RequestInit;
export type SchemaOutput<A extends StandardSchemaV1> =
StandardSchemaV1.InferOutput<A>;
export type SafeFetchError<E extends StandardSchemaV1> =
| ApiError<E>
| ParseError
| UnexpectedError;
export type SafeFetchResult<
SuccessSchema extends StandardSchemaV1,
FailureSchema extends StandardSchemaV1,
> = ResultAsync<SchemaOutput<SuccessSchema>, SafeFetchError<FailureSchema>>;
export async function safeFetch<
Success extends StandardSchemaV1,
Failure extends StandardSchemaV1,
>(
url: string,
options: SafeFetchOptions<Success, Failure>,
): Promise<SafeFetchResult<Success, Failure>> {
try {
const response = await fetch(url, options);
const schema = response.ok ? options.successSchema : options.failureSchema;
const data = await response.json();
const parsed = await schema['~standard'].validate(data);
if (parsed.issues !== undefined) {
return errAsync(
new ParseError({
response,
issues: parsed.issues,
}),
);
}
return response.ok
? okAsync(parsed)
: errAsync(new ApiError({ error: parsed }));
} catch (cause) {
return errAsync(new UnexpectedError({ cause }));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment