We’ve seen how Effect.catchAll serves as a basic way to handle errors in many different ways. As we build up more complex programs, the number of possible errors accumulates.
Effect.catchTag allows us to handle one particular type of error at a time, rather than needing to deal with all of them.
Let’s revisit our HTTP client from the Effect.tryPromise lesson.
import { Effect } from "effect";
class RequestFailure extends Error { _tag = "RequestFailure" as const;
constructor(readonly url: string) { super(`unable to get ${url}`); }}
class ResponseFailure extends Error { _tag = "ResponseFailure" as const;
constructor(readonly response: Response) { super(response.statusText); }}
const get = (url: string) => Effect.tryPromise({ try: () => fetch(url), catch: () => new RequestFailure(url), }).pipe( Effect.flatMap((response) => Effect.tryPromise({ try: (): Promise<unknown> => response.json(), catch: () => new ResponseFailure(response), }), ), );There are some small changes. Most importantly, the error types each define an _tag attribute. We will use this as the “discriminator”, a way of differentiating between multiple kinds of error.
Let’s use Effect.catchTag to handle the two types of error differently.
const makeProgram = (url: string) => get(url).pipe( Effect.map((json) => console.log(json)), Effect.catchTag("RequestFailure", (e) => Effect.succeed(console.error(`${e.url} is not available`)), ), Effect.catchTag("ResponseFailure", (e) => Effect.succeed( console.error( `request failed with status ${e.response.status} (${e.response.statusText})`, ), ), ), );
const success = makeProgram("https://httpbin.org/uuid");const badResponse = makeProgram("https://httpbin.org/status/418");const badRequest = makeProgram("no-way");
const main = async () => { console.log("=== running 1 ==="); await Effect.runPromise(success); console.log("=== running 2 ==="); await Effect.runPromise(badResponse); console.log("=== running 3 ==="); await Effect.runPromise(badRequest); console.log("=== done ===");};
main();Note that inside each error handler we are able to access attributes relevant to that particular error: e.url is available on RequestFailure and e.response is available on ResponseFailure. This is because each of the errors has a distinct _tag attribute.
It is common to give each error class its own unique tag, so that it can be identified. As we will see in future lessons, this pattern is used for many other data types within the Effect library. In fact, we already saw it back in the very first lesson on Effect.succeed:
const myFirstEffect = Effect.succeed(3)console.log(myFirstEffect)// { _id: 'Exit', _tag: 'Success', value: 3 }The Effect type is “tagged” in this same way, and this is how Effect differentiates between Success and Failures under the hood.
We could have written the above code using Effect.catchAll (8)
:
const makeProgram = (url: string) => get(url).pipe( Effect.map((json) => console.log(json)), Effect.catchAll((e) => { if (e._tag === "RequestFailure") { return Effect.succeed(console.error(`${e.url} is not available`)); } if (e._tag === "ResponseFailure") { return Effect.succeed( console.error( `request failed with status ${e.response.status} (${e.response.statusText})`, ), ); } return Effect.fail(e); }), );As you can see, the extra if statements make this clunky. Effect.catchAll (8)
is great for being so versatile, but Effect.catchTag is much simpler (to read and to write) in this common error handling situation. You can think of Effect.catchTag as being a useful shortcut.
You can implement catchTag using catchAll. The type definitions are not at all obvious, but the logic is simple.
type WithTag<E, T> = E extends {_tag: T} ? E : never
const _isTagged = <E, T>(t: T, e: E): e is WithTag<E, T> => (e as any)?.tag === t;
const catchTag = <T extends string, E, A, R>( tag: T, handler: (e: WithTag<E, T>) => Effect.Effect<A, Exclude<E, WithTag<E, T>>, R>,) => Effect.catchAll((e: E) => { if (_isTagged(tag, e)) { return handler(e); } return Effect.fail(e); });Effect has many error types internally, and these are also tagged. For example, we have already seen UnknownException.
const input = "not-a-url";const tried = Effect.try(() => new URL(input as any));
tried.pipe( Effect.catchTag("UnknownException", (e) => Effect.succeed(`${e.message} because ${e.error}`), ), Effect.map(console.log), Effect.runSync,);Another common convention is to namespace the error tag. This improves the chances of a tag being “globally” unique, avoiding accidental clashes. For example, if your application had its own UnknownException then you might want to tag that @myapp/UnknownException to avoid clashing with Effect’s error with that tag.
Just like with Effect.catchAll (8)
, the response from Effect.catchTag is another Effect, so the result may be to fail with a different result.
const input = "not-a-url";const tried = Effect.try(() => new URL(input as any));
class InvalidUrl extends Error { readonly _tag = "@fred.c/InvalidUrl" as const; readonly message = "try a better URL next time";}
tried.pipe( Effect.catchTag("UnknownException", () => Effect.fail(new InvalidUrl())), Effect.map((url) => console.log(url)), // ┌─── InvalidUrl (not UnknownException) // ▼ Effect.catchAll((e) => Effect.succeed(console.error(e))), Effect.runSync,);