Effect.try

We have now met most of the main functions you need to work with synchronous Effects. There are many more functions in the Effect module, but you can get a long way with the building blocks of Effect.succeed, Effect.fail, Effect.map, Effect.flatMap and Effect.catchAll.

Effect-the-library exposes a lot of additional functions as shortcuts. After all, one of the main aims of the Effect ecosystem is to help us be more productive. Let’s take a look at one such convenient helper, Effect.try.

You may recall this code from a previous lesson.

import { Effect } from "effect";

class InvalidUrl extends Error {
	_kind = "InvalidUrl" as const;
}

const input = "bad-url";

const decodeUrlFromUnknown = (x: unknown): Effect.Effect<URL, InvalidUrl> => {
    try {
        return Effect.succeed(new URL(x as any));
    } catch (e) {
        return Effect.fail(new InvalidUrl(`${x} is not a URL`));
    }
};

decodeUrlFromUnknown(input).pipe(
    //           ┌─── URL
    //           ▼
    Effect.map((url) => `The hostname is ${url.hostname}`),
    Effect.map(console.log),
    //               ┌─── InvalidUrl
    //               ▼
    Effect.catchAll((e) => Effect.succeed(console.error(e))),
    Effect.runSync,
);

We can write decodeUrlFromUnknown much more succinctly.

const decodeUrlFromUnknown = (x: unknown) =>
    Effect.try(() => new URL(x as any));

decodeUrlFromUnknown(input).pipe(
    Effect.map((url) => `The hostname is ${url.hostname}`),
    Effect.map(console.log),
    //               ┌─── UnknownException
    //               ▼
    Effect.catchAll((e) => Effect.succeed(console.error(e.error))),
    Effect.runSync,
);

This constructor looks similar to Effect.sync: it accepts a thunk (function with no arguments) and returns an Effect. There is one key difference: the return type clearly indicates that something can go wrong.

const sync: <URL>(thunk: LazyArg<URL>) => Effect.Effect<URL, never, never>
const try: <URL>(thunk: LazyArg<URL>) => Effect.Effect<URL, UnknownException, never>

Effect will automatically wrap the function in a try ... catch block, and fail with an UnknownException. This UnknownException wraps around whatever value was thrown. This is the best that TypeScript can do: there is no way of inferring what was actually thrown.

Effect.try exposes an alternative format, where we can specify how the program can go wrong, not just that it can go wrong.

const decodeUrlFromUnknown = (x: unknown) =>
    Effect.try({
        try: () => new URL(x as any),
        catch: (e) => new InvalidUrl(`${x} is not a URL`),
    });

decodeUrlFromUnknown(input).pipe(
    Effect.map((url) => `The hostname is ${url.hostname}`),
    Effect.map(console.log),
    //               ┌─── InvalidUrl
    //               ▼
    Effect.catchAll((e) => Effect.succeed(console.error(e))),
    Effect.runSync,
);

Although the first form is shorter, the second form is more common in practice. Being explicit is very helpful as documentation. We saw in the lesson on Effect.catchAll how helpful it is to be able to distinguish between different error types.

There is one other difference to note between Effect.sync and Effect.try. You should only ever use Effect.sync for operations that will definitely succeed. Any errors thrown while running that synchronous function will not be caught in the way you might expect. Again, recall that Effects are lazy, and that creating an Effect is separate from running it.

const decodeUrlFromUnknown = (x: unknown): Effect.Effect<URL, InvalidUrl> => {
    try {
        // note: this returns once it has _created_ the Effect
        // (not when it has _run_ the Effect)
        return Effect.sync(() => new URL(x as any));
    } catch (e) {
        // (this code will not run: because we can _create_ the Effect successfully)
        return Effect.fail(new InvalidUrl(`${x} is not a URL`));
    }
};

try {
    decodeUrlFromUnknown(input).pipe(
        //           ┌─── URL
        //           ▼
        Effect.map((url) => `The hostname is ${url.hostname}`),
        Effect.map(console.log),
        //               ┌─── InvalidUrl
        //               ▼
        Effect.catchAll((e) =>
            Effect.succeed(console.error("handled inside:", e)),
        ),
        Effect.runSync,
    );
} catch (e) {
    console.error("the error leaked outside our Effect boundary!", e);
}

Let’s recap:

  • Effect.try allows you to wrap operations that may fail
  • The thunk form of Effect.try may fail with an UnknownException
  • The object form of Effect.try allows you to specify custom error handling logic
  • The object form allows you to define explicit failure conditions for your program
  • Documenting failures explicitly in the type signature can help you write more robust software