Skip to content

Instantly share code, notes, and snippets.

@RaniAgus
Last active February 7, 2026 05:10
Show Gist options
  • Select an option

  • Save RaniAgus/c3cb2622fecc127e1e32a74b8c77f7f8 to your computer and use it in GitHub Desktop.

Select an option

Save RaniAgus/c3cb2622fecc127e1e32a74b8c77f7f8 to your computer and use it in GitHub Desktop.
Effect-like error matching with neverthrow

For context, I'm using Error as my "defect" channel and tagged types as my "error" channel.

At the moment, creating just the async version of it was enough for me, but you could create a sync matchTag and matchAllTags and then also rename these to matchTagAsync and matchAllTagsAsync.

There is also another caveat: both functions don't accept handlers that return tagged errors. For my use case, which is handling all possible errors before unwrapping a ResultAsync<T, Error>, I didn't need to handle that scenario.

Example usage:

function getUserDetails(req: Request): ResultAsync<User, NoTokenError | ExpiredTokenError | Error>;

function handler(req: Request): ResultAsync<Response | User, Error> {
  return safeTry(async function* () {
      const userDetails = yield* getUserDetails(req);

      if (userDetails.roles.includes("ADMIN")) {
        return okAsync(redirect("/404"));
      }

      return okAsync(userDetails);
    }).orElse(
      matchAllTags({
        no_token: () => okAsync(redirect("/404")),
        expired_token: () => okAsync(redirect("/login")),
      }),
    );
}
import { type ResultAsync, errAsync } from "neverthrow";
// Utility to match a single tag in a type-safe way, returning a ResultAsync with all errors except the matched ones
export function matchTag<E extends { _tag: string }, K extends E["_tag"], R>(
tag: K,
handler: (error: Extract<E, { _tag: K }>) => ResultAsync<R, Error>,
) {
return (
error: E | Error,
):
| ReturnType<typeof handler>
| ResultAsync<never, Exclude<E, { _tag: K }>>
| ResultAsync<never, Error> => {
if (error instanceof Error) {
return errAsync(error) as ResultAsync<never, Error>;
}
if (error._tag === tag) {
return handler(error as Extract<E, { _tag: K }>);
}
return errAsync(error as Exclude<E, { _tag: K }>) as ResultAsync<
never,
Exclude<E, { _tag: K }>
>;
};
}
// Utility to match all error tags in a type-safe way
export function matchAllTags<
E extends { _tag: string },
Cases extends {
[K in E["_tag"]]: (
error: Extract<E, { _tag: K }>,
) => ResultAsync<any, Error>;
},
>(cases: Cases) {
return (
error: E | Error,
): ReturnType<Cases[keyof Cases]> | ResultAsync<never, Error> => {
if (error instanceof Error) {
return errAsync(error) as ResultAsync<never, Error>;
}
const handler = cases[error._tag as keyof Cases];
if (handler) {
return handler(error as any) as ReturnType<Cases[keyof Cases]>;
}
return errAsync(
new Error(`Unhandled error: ${error._tag}`, { cause: error }),
) as ResultAsync<never, Error>;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment