Effect.catchAll

In previous lessons we have seen how Effects can go wrong, and that each failure can be documented in the error type, but all our error handling was pushed outside of Effect. Effect.catchAll allows us to catch any errors thrown by our program so far, and execute an alternative program.

Let’s see the problem first

import { Effect } from "effect";


class ValidationError extends Error {
	kind = "ValidationError" as const;
}

const isString = (x: unknown) =>
	typeof x === "string"
		? Effect.succeed(x)
		: Effect.fail(new ValidationError(`${x} is not a string`));

const isUrl = (x: string) => {
	try {
		return Effect.succeed(new URL(x));
	} catch (e) {
		return Effect.fail(new ValidationError(`${x} is not a URL`));
	}
};

const minLength = (length: number) => (x: string) =>
	x.length >= length
		? Effect.succeed(x)
		: Effect.fail(new ValidationError(`${x} is too short`));

const validateHostname = (value: unknown) =>
	Effect.succeed(value).pipe(
		Effect.flatMap(isString),
		Effect.flatMap(minLength(5)),
		Effect.map((s) => s.toLowerCase()),
		Effect.flatMap(isUrl),
		Effect.map((url) => url.hostname),
	);

// problem 1
validateHostname("https://EFFECT.website").pipe(
	Effect.map(console.log),
	Effect.runSync,
);

try {
	validateHostname(1234).pipe(Effect.map(console.log), Effect.runSync);
} catch (e) {
    // problem 2
	console.error(e);
}

There are two problems. Firstly, we do not have to define any error handling logic. Effect.runSync will gladly run a program that can go wrong, and convert those failures to exceptions. This can be a valid error handling strategy - there are some errors that you cannot or should not recover from.

The second problem is that we lose our type safety as soon as the program is run. The rest of our code, outside of Effect.runSync, is not aware of the error types. This means that e is of type unknown. This lack of type safety is one of the main reasons why error handling code has traditionally ended up being unwieldy or poorly written.

We can use Effect.catchAll to catch all errors, returning a new Effect that will not fail.

const initialValue = 1234;
const succeeded = validateHostname(initialValue).pipe(
	Effect.map((hostname) => {
		console.log(`The hostname is ${hostname}`);
		return true;
	}),
	Effect.catchAll((err) => {
        console.error(`Failed because ${err.message}`);
        return Effect.succeed(false)
    }),
	Effect.runSync,
);

console.log(`The program ${succeeded ? "succeeded" : "failed"}.`);

Effect.catchAll takes a plain value (the error) and returns an Effect. In this sense, it is similar to flatMap but for error types. You can chain multiple Effect.catchAll calls together if, for example, you have handlers that each recover from one particular type of error.

Note that Effect.catchAll can return any Effect, not just a successful one. This gives us a lot of flexibility with our error handling strategy.

Use Effect.catchAll to transform the error:

class MissingEnvironmentVariable extends Error {
    kind = "MissingEnvironmentVariable" as const;
}

const getEnvironmentVariable = (key: string) =>
    Effect.sync(() => process.env[key]).pipe(
        Effect.flatMap(isString),
        Effect.catchAll(() =>
            Effect.fail(new MissingEnvironmentVariable(`${key} not set`)),
        ),
    );

// getEnvironmentVariable("PORT").pipe(
//     Effect.map(console.log),
//     Effect.runSync,
// );

In the example above, isString may fail with a ValidationError in the case where the environment variable is not set. This would be a confusing result to get from getEnvironmentVariable, so we can replace the failure entirely.

Use Effect.catchAll to provide a default value:

const logLevel = getEnvironmentVariable("LOG_LEVEL").pipe(
    Effect.catchAll(() => Effect.succeed("INFO")),
)

// logLevel.pipe(
//     Effect.map(level => console.log(`logging at ${level}`)),
//     Effect.runSync,
// );

Use Effect.catchAll to perform some work, but letting the error continue to bubble up:

const databaseUrl = getEnvironmentVariable("DATABASE_URL").pipe(
    Effect.catchAll((err) => {
        console.error("You must set DATABASE_URL as an environment variable.")
        return Effect.fail(err)
    }),
);

// databaseUrl.pipe(
//     Effect.map(url => console.log(`the database is at ${url}`)),
//     Effect.runSync,
// );

Use Effect.catchAll to suppress the error completely:

validateHostname("bad-url").pipe(
    Effect.map(hostname => `The hostname is ${hostname}`),
    Effect.catchAll((err) => {
        console.error("Something went wrong.", err);
        return Effect.succeed(undefined);
    }),
    Effect.runSync
);

Use Effect.catchAll to catch some errors, but not others:

const decodeBooleanFromString = (
    x: string,
): Effect.Effect<boolean, ValidationError> => {
    const cleaned = x.toLowerCase();
    switch (cleaned) {
        case "true":
        case "1":
            return Effect.succeed(true);
        case "false":
        case "0":
            return Effect.succeed(false);
        default:
            return Effect.fail(new ValidationError("cannot parse value"));
    }
};

// use the environment variable,
// or false if no variable provided,
// or fail if variable provided with invalid valoue
const isDebugMode = getEnvironmentVariable("DEBUG").pipe(
    Effect.flatMap(decodeBooleanFromString),
    Effect.catchAll((e) => {
        // recover from MissingEnvironmentVariable 
        if (e.kind === "MissingEnvironmentVariable") {
            return Effect.succeed(false);
        }
        // but not from ValidationError
        return Effect.fail(e);
    }),
)

isDebugMode.pipe(
    Effect.map((debug) =>
        console.log(`Running app in ${debug ? "debug" : "production"} mode`),
    ),
    // catch and log remaining errors (ValidationError)
    Effect.catchAll((e) => Effect.succeed(console.error(e))),
    Effect.runSync,
);

As you can see, error handling logic is not straightforward. The right way to handle errors depends on the situation. Can your program recover? Should the operation be retried? With the same parameters? Or with different parameters?

Effect.catchAll is a very general purpose way to handle errors. In future lessons we will see many more error handling helpers, but for now Effect.catchAll will be enough.

Let’s recap:

  • Effect-the-library includes tools to make error-handling simpler to reason about.
  • Effect-the-data-type is a data type with up to 3 generic parameters.
  • The second parameter represents failures that can occur when this Effect is run.
  • Effect.catchAll allows us to provide a successful fallback if an operation fails, letting the remaining operations continue.
  • Effect.catchAll transforms a plain value into an Effectful value - a bit like Effect.flatMap, but operating on failures.
  • Effect-the-ecosystem encourages us to think carefully about error handling logic, to write more robust software.