orj

Result: errors as values

Result<E, T> is an operation that either succeeded with a T or failed with an E. Both outcomes are ordinary values, so failure handling becomes plain code you can read, type-check, and test, instead of a control-flow surprise three stack frames up.

Errors are typed. Instead of String messages or a grab-bag Exception, model the ways an operation can fail as a small sealed hierarchy:

sealed interface ParseError {
    record NotANumber(String raw) implements ParseError {}
    record NotPositive(int value) implements ParseError {}
}
static Result<ParseError, Integer> parsePositive(String raw) {
    int n;
    try {
        n = Integer.parseInt(raw);
    } catch (NumberFormatException e) {
        return Result.err(new ParseError.NotANumber(raw));
    }
    return n > 0 ? Result.ok(n) : Result.err(new ParseError.NotPositive(n));
}

Pattern matching is the API

Result is a sealed interface with two record cases, Ok and Err. A switch over it is exhaustive: handle both cases or the compiler stops you. Combinators exist for chains, but this is the primary interface. There is no method to memorize, just the language.

String message = switch (parsePositive("-3")) {
    case Result.Ok<ParseError, Integer> ok -> "got " + ok.value();
    case Result.Err<ParseError, Integer> err -> switch (err.error()) {
        case ParseError.NotANumber(String raw) -> raw + " is not a number";
        case ParseError.NotPositive(int n) -> n + " is not positive";
    };
};

assertEquals("-3 is not positive", message);

Transform without unwrapping

map transforms a success and leaves an error untouched; flatMap chains another fallible step; recover turns an error back into a success. Chains stop doing work at the first Err. No null checks, no nesting.

Result<ParseError, Integer> doubled = parsePositive("21").map(n -> n * 2);
assertEquals(Result.ok(42), doubled);

Result<ParseError, Integer> chained = parsePositive("6")
        .flatMap(n -> parsePositive(String.valueOf(n - 7)))   // -1 -> Err
        .map(n -> n * 100);                                    // never runs
assertEquals(Result.err(new ParseError.NotPositive(-1)), chained);

assertEquals(Result.ok(0), chained.recover(e -> Result.ok(0)));

Wrap code that throws

The world is full of APIs that throw. attempt runs a block, catches anything thrown, and maps it into your error type. Several dependent throwing calls compose as straight-line code, and the first throw short-circuits the rest.

Result<String, Integer> r = Result.attempt(() -> {
    int a = mustParse("12");        // throws on bad input
    int b = mustParse("oops");      // throws here...
    return a + b;                   // ...so this never runs
}, t -> "failed: " + t.getMessage());

assertEquals(Result.err("failed: not a number: oops"), r);
static int mustParse(String raw) throws IOException {
    try {
        return Integer.parseInt(raw);
    } catch (NumberFormatException e) {
        throw new IOException("not a number: " + raw);
    }
}

Sequence Result-returning steps with binding

When steps already return Result, binding gives you do-notation: bind.on(...) hands back the raw success value, and the first Err aborts the whole block and becomes its result. No flatMap towers, and every earlier value stays in scope for later steps.

Result<ParseError, Integer> total = Result.binding(bind -> {
    int a = bind.on(parsePositive("3"));
    int b = bind.on(parsePositive("4"));
    int c = bind.on(parsePositive("5"));
    return a * b * c;
});

assertEquals(Result.ok(60), total);

Guards without values: ensure

Not every rule produces a value; some just have to hold. bind.ensure reads like an assertion but fails like everything else here: as a typed Err that short-circuits the block.

Result<String, Integer> withdrawal = Result.binding(bind -> {
    int balance = bind.on(Result.<String, Integer>ok(70));
    bind.ensure(balance >= 100, () -> "insufficient funds: " + balance);
    return balance - 100;                  // never reached
});

assertEquals(Result.err("insufficient funds: 70"), withdrawal);

Bridge to and from Optional

Optional models absence; Result models failure with a reason. The bridges keep the line crisp: lifting an empty Optional into a Result asks you to name the failure, because emptiness alone doesn't carry one.

Optional<NonEmptyList<Integer>> none = NonEmptyList.fromList(List.of());

Result<String, NonEmptyList<Integer>> r =
        Result.fromOptional(none, () -> "no candidates to rank");

assertEquals(Result.err("no candidates to rank"), r);
assertEquals(Optional.of("no candidates to rank"), r.errValue());

This page is generated from a test file. Read or improve it on GitHub.