In previous lessons we introduced Effect.try and Effect.promise. Effect.try
(9)
wraps a (synchronous) computation that may fail. Effect.promise
(11)
wraps an asynchronous computation, but without handling failure. Effect.tryPromise
lets us do both.
First, let’s see what goes wrong with Effect.promise
(11)
.
import { Effect } from 'effect';
// ┌─── Effect.Effect<Response, never, never>// ▼const fail = Effect.promise(() => fetch("not-a-url"));
fail .pipe( // ┌─── never // ▼ Effect.catchAll((e) => Effect.succeed(console.error("handled inside:", e))), Effect.runPromise, ) .then((x) => console.log("program ran to completion with", x)) .catch((e) => console.error("the error leaked outside our Effect boundary!", e), );
In this example, the catchAll
handler never runs, because the error is thrown without Effect’s type system knowing about it. Recall from Effect.fail that Effect treats Failures and Defects differently. This error is treated as a Defect, rather than a Failure. In some situations, allowing the error to bubble up untracked and crash the entire program is a valid error handling strategy.
$ npx tsx main.tsthe error leaked outside our Effect boundary! (FiberFailure) TypeError: Failed to parse URL from not-a-url at node:internal/deps/undici/undici:12345:11
Now let’s write the same code using Effect.tryPromise
.
// ┌─── Effect.Effect<Response, UnknownException, never>// ▼const fail = Effect.tryPromise(() => fetch("not-a-url"));// was: const fail = Effect.promise(() => fetch("not-a-url"));
// remainder unchangedfail .pipe( // ┌─── UnknownException // ▼ Effect.catchAll((e) => Effect.succeed(console.error("handled inside:", e)), ), Effect.runPromise, ) .then((x) => console.log("program ran to completion with", x)) .catch((e) => console.error("the error leaked outside our Effect boundary!", e), );
If we run this, we see the catchAll
handler runs successfully and the program runs to completion.
$ npx tsx main.txhandled inside: UnknownException: An unknown error occurred at e (/home/fred/dev/learn-effect/node_modules/.pnpm/effect@3.12.0/node_modules/effect/src/internal/core-effect.ts:1669:80) { cause: TypeError: Failed to parse URL from not-a-url at node:internal/deps/undici/undici:12345:11 { [cause]: TypeError: Invalid URL at new URL (node:internal/url:775:36) at new Request (node:internal/deps/undici/undici:5853:25) at fetch (node:internal/deps/undici/undici:10123:25) at Object.fetch (node:internal/deps/undici/undici:12344:10) at fetch (node:internal/process/pre_execution:336:27) at <anonymous> (/home/fred/dev/learn-effect/scratch.ts:5:38) at resolve (/home/fred/dev/learn-effect/node_modules/.pnpm/effect@3.12.0/node_modules/effect/src/internal/core-effect.ts:1666:7) at <anonymous> (/home/fred/dev/learn-effect/node_modules/.pnpm/effect@3.12.0/node_modules/effect/src/internal/core.ts:569:46) at Object.effect_internal_function (/home/fred/dev/learn-effect/node_modules/.pnpm/effect@3.12.0/node_modules/effect/src/Utils.ts:780:14) at <anonymous> (/home/fred/dev/learn-effect/node_modules/.pnpm/effect@3.12.0/node_modules/effect/src/Utils.ts:784:22) { code: 'ERR_INVALID_URL', input: 'not-a-url' } }, _tag: 'UnknownException', error: TypeError: Failed to parse URL from not-a-url at node:internal/deps/undici/undici:12345:11 { [cause]: TypeError: Invalid URL at new URL (node:internal/url:775:36) at new Request (node:internal/deps/undici/undici:5853:25) at fetch (node:internal/deps/undici/undici:10123:25) at Object.fetch (node:internal/deps/undici/undici:12344:10) at fetch (node:internal/process/pre_execution:336:27) at <anonymous> (/home/fred/dev/learn-effect/scratch.ts:5:38) at resolve (/home/fred/dev/learn-effect/node_modules/.pnpm/effect@3.12.0/node_modules/effect/src/internal/core-effect.ts:1666:7) at <anonymous> (/home/fred/dev/learn-effect/node_modules/.pnpm/effect@3.12.0/node_modules/effect/src/internal/core.ts:569:46) at Object.effect_internal_function (/home/fred/dev/learn-effect/node_modules/.pnpm/effect@3.12.0/node_modules/effect/src/Utils.ts:780:14) at <anonymous> (/home/fred/dev/learn-effect/node_modules/.pnpm/effect@3.12.0/node_modules/effect/src/Utils.ts:784:22) { code: 'ERR_INVALID_URL', input: 'not-a-url' } }}
Just like with Effect.try
(9)
, we see an UnknownException
. And, just like Effect.try
(9)
, we can use the object form to specify custom error handling right at the source.
class FetchFailure extends Error { _kind = "FetchFailure" as const;}
// ┌─── Effect.Effect<Response, FetchFailure, never>// ▼const fail = Effect.tryPromise({ try: async () => fetch("not-a-url"), catch: (e) => new FetchFailure(`failed to fetch because ${e}`),});
// code unchanged, but the error type has changedfail .pipe( // ┌─── FetchFailure // ▼ Effect.catchAll((e) => Effect.succeed(console.error("handled inside:", e)), ), Effect.runPromise, ) .then((x) => console.log("program ran to completion with", x)) .catch((e) => console.error("the error leaked outside our Effect boundary!", e), );
Let’s see what a basic HTTP client might look like.
class RequestFailure extends Error { _kind = "RequestFailure" as const;}
class ResponseFailure extends Error { _kind = "ResponseFailure" as const;}
const get = (url: string) => Effect.tryPromise({ try: () => fetch(url), catch: (e) => new RequestFailure(`unable to get ${url} because ${e}`), }).pipe( Effect.flatMap((response) => Effect.tryPromise({ try: (): Promise<unknown> => response.json(), catch: () => new ResponseFailure(response.statusText), }), ), );
const success = get("https://httpbin.org/uuid").pipe( Effect.map((json) => console.log(json)), Effect.catchAll((e) => Effect.succeed(console.error(e))),);
const badResponse = get("https://httpbin.org/status/418").pipe( Effect.map((json) => console.log(json)), Effect.catchAll((e) => Effect.succeed(console.error(e))),);
const badRequest = get("no-way").pipe( Effect.map((json) => console.log(json)), Effect.catchAll((e) => Effect.succeed(console.error(e))),);
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();
This is a big improvement over what JavaScript gives us out of the box. We get a type hint letting us know that our request handler can go wrong, and allowing us to handle that error in a type-safe way. Not only that, but we see that there are multiple ways it could go wrong: RequestFailure | ResponseFailure
.
This guides our error handling strategy. If we got a ResponseFailure
, perhaps it makes sense to retry immediately. If we got a RequestFailure
, perhaps the best option is to show the user a message saying “please try again later”.
We could go further. Our current ResponseFailure
is quite broad. It could be split to separate BadHttpStatus | InvalidJson
. We could go even more granular, with errors able to distinguish between each of the HTTP status codes. This distinction could be done as a property of the BadHttpStatus
error. It might be overwhelming to have separate classes for HttpBadRequest | HttpNotFound | HttpInternalServerError | ...
.
We could also pull out error handling logic as a generic function.
As the Effect community grows, there will be more and more wrappers around commonly used utilities like fetch
. There is a FetchHttpClient
implementation in the @effect/platform
package, but at time of writing this package is experimental.