jabrena / typed-errors

A Java library to help developers on `Error handling` using functional programming techniques and new Java Types.
https://jabrena.github.io/typed-errors/javadoc/
Apache License 2.0
1 stars 0 forks source link

Typed errors

Java CI

SonarCloud

Cloud IDEs

Open in GitHub Codespaces

How to build in local?

sdk env install
./mvnw prettier:write

./mvnw clean verify 
./mvnw clean test -Dtest=StructuredTest

//Code coverage
./mvnw clean verify jacoco:report
jwebserver -p 9000 -d "$(pwd)/target/site/jacoco/"
./mvnw clean verify org.pitest:pitest-maven:mutationCoverage

//Javadoc
./mvnw clean compile javadoc:javadoc
./mvnw verify -DskipTests -P post-javadoc
jwebserver -p 9001 -d "$(pwd)/docs/javadocs/"

./mvnw versions:display-property-updates
./mvnw versions:display-dependency-updates
./mvnw versions:display-plugin-updates
./mvnw dependency:tree

Introduction

The Java programming language was designed with Exceptions in mind as the way to handle events that disrupts the normal flow of a program's execution. These exceptions can occur during the runtime of a program and can be caused by various issues such as incorrect input, network problems, or hardware malfunctions.

Exceptions in Java are represented by objects from classes that extend the Throwable class. There are two main types of exceptions in Java: checked exceptions and unchecked exceptions. Checked exceptions are checked at compile time, while unchecked exceptions are not.

Handling exceptions properly is important for writing robust and maintainable Java programs. It helps in dealing with unexpected situations effectively and ensures that the program does not crash or terminate abruptly.

Problem statement

Using the following Gherkin feature for educational purposes and the different implementations, they will show the current Java modelling problem without using Typed Errors and how this library could help you to design & implement better software.

Feature: Convert String into Integer

  # Happy path
  Scenario: The user introduce valid String values
    Given a String as a parameter
    When when it is passed to the method to convert String into Integer
    Then it returns a valid Integer

    Examples
    | input | output |
    |  "1"  |   1    |
    |  "2"  |   2    |
    | "-1"  |  -1    |

  # Unhappy path: ASCII Characters
  Scenario: Introducing ASCII characters
    Given a String as a parameter
    When when it is passed to the method to convert String into Integer
    Then returns an Exception

    Examples
    | input | output |
    |  "A"  |  KO    |
    |  "B"  |  KO    |
    |  "z"  |  KO    |

  # Unhappy path: Symbols
  Scenario: Introducing symbols
    Given a String as a parameter
    When when it is passed to the method to convert String into Integer
    Then returns an Exception

    Examples
    | input | output |
    |  "."  |  KO    |
    |  ","  |  KO    |
    |  "!"  |  KO    |

  # Unhappy path: Numbers out of Integer Data Type range (-2^31-1 <--> 2^31-1)
  Scenario: Reaching the limit of Integer
    Given a String as a parameter
    When when it is passed to the method to convert String into Integer
    Then returns an Exception

    Examples
    | input          | output |
    |  "2147483648"  |  KO    |
    |  "-2147483649" |  KO    |

After reading the the Specification, you could implement in the following way in Java in an initial way:

Function<String, Integer> parseInt = param -> {
    try {
        return Integer.parseInt(param);
    } catch (NumberFormatException ex) {
        logger.warn(ex.getMessage(), ex);
        return -99;
    }
};

But in this case, the implementation adds side effects in case of the user introduce a non positive numbers or larger negative numbers than -99.


So maybe you could use Java Exceptions, as another implementation:

Function<String, Integer> parseInt2 = param -> {
    try {
        return Integer.parseInt(param);
    } catch (NumberFormatException ex) {
        logger.warn(ex.getMessage(), ex);
        throw new RuntimeException("Katakroker", ex);
    }
};

But using this way, the signature changes because in some cases, the implementation trigger an Exception and it could be considered as a another way of GOTO.


In Java 8, the lenguage evolved and it included Wrapper Types like Optional which it is valid to describe that the result could be present or not.

One possible implementation could be:

Function<String, Optional<Integer>> parseInt3 = param -> {
    try {
        return Optional.of(Integer.parseInt(param));
    } catch (NumberFormatException ex) {
        logger.warn(ex.getMessage(), ex);
        return Optional.empty();
    }
};

But reviewing the implementation, Optional was not designed to model Errors, it was modelled to describe the presence or absence of a result.


Finally, you could consider to use other kind of Wrapper Types to describe that your method/function could return a valid result or an error.

This approach is very common in plenty programming languages like: Scala, Kotlin, TypeScript, Rust, Golang, Swift, Haskell, Unison, Ocaml or F#.

So, using the prevoius idea to model your method to return a value or an error, you could implement with Either in this way:

enum ConversionIssue {
    BAD_STRING,
}

Function<String, Either<ConversionIssue, Integer>> parseInt4 = param -> {
    try {
        return Either.right(Integer.parseInt(param));
    } catch (NumberFormatException ex) {
        logger.warn(ex.getMessage(), ex);
        return Either.left(ConversionIssue.BAD_STRING);
    }
};

or using Result:

Function<String, Result<Integer>> parseInt5 = param -> {
    return Result.runCatching(() -> {
        return Integer.parseInt(param);
    });
};

So, if you followed the previous examples and you understood the concepts behind them, now you understand the purpose of this Java library.

Library Goal

A Java library to help developers on Error handling using functional programming techniques and new Java Types.

Error types

Either<L, R>

Either<L, R> is a commonly used data type that encapsulates a value of one of two possible types. It represents a value that can be either an "error" (left) or a "success" (right). This is particularly useful for error handling and avoiding exceptions.

Either examples

//1. Learn to instanciate an Either object.
enum ConnectionProblem {
    INVALID_URL,
    INVALID_CONNECTION,
}

Either<ConnectionProblem, String> resultLeft = Either.left(ConnectionProblem.INVALID_URL);
Either<ConnectionProblem, String> resultRight = Either.right("Success");

Either<ConnectionProblem, String> eitherLeft = new Left<>(ConnectionProblem.INVALID_CONNECTION);
Either<ConnectionProblem, String> eitherRight = new Right<>("Success");

//2. Learn to use Either to not propagate Exceptions any more
Function<String, Either<ConnectionProblem, URI>> toURI = address -> {
    try {
        var uri = new URI(address);
        return Either.right(uri);
    } catch (URISyntaxException | IllegalArgumentException ex) {
        logger.warn(ex.getLocalizedMessage(), ex);
        return Either.left(ConnectionProblem.INVALID_URL);
    }
};

//3. Process results
Function<Either<ConnectionProblem, URI>, String> process = param -> {
    return switch (param) {
        case Right<ConnectionProblem, URI> right -> right.get().toString();
        case Left<ConnectionProblem, URI> left -> "";
    };
};

var case1 = "https://www.juanantonio.info";
var result = toURI.andThen(process).apply(case1);
System.out.println("Result: " + result);

Function<Either<ConnectionProblem, URI>, String> process2 = param -> {
    return param.fold(l -> "", r -> r.toString());
};

var case2 = "https://";
var result2 = toURI.andThen(process2).apply(case2);
System.out.println("Result: " + result2);

//Guarded patterns
//any guarded pattern makes the switch statement non-exhaustive
Function<Either<ConnectionProblem, URI>, String> process3 = param -> {
    return switch (param) {
        case Either e when e.isRight() -> e.get().toString();
        default -> "";
    };
};

var case3 = "https://www.juanantonio.info";
var result3 = toURI.andThen(process3).apply(case3);
System.out.println("Result: " + result3);

//4. Railway-oriented programming

Function<String, Either<String, String>> validateTopLevelDomain = email -> {
    String tld = email.substring(email.lastIndexOf('.') + 1);
    if (tld.length() != 3) {
        return Either.left("Invalid top-level domain");
    }
    return Either.right(email);
};

Function<String, Either<String, String>> validateUsername = email -> {
    String username = email.substring(0, email.indexOf('@'));
    if (username.length() < 5) {
        return Either.left("Username must be at least 5 characters");
    }
    return Either.right(email);
};

Function<String, Either<String, String>> validateDomain = email -> {
    String domain = email.substring(email.indexOf('@') + 1);
    if (!domain.contains(".")) {
        return Either.left("Invalid domain format");
    }
    return Either.right(email);
};

// @formatter:off

Function<String, Either<String, String>> validateEmail = email -> {
    return validateUsername.apply(email)
        .flatMap(validUsername -> validateDomain.apply(email))
        .flatMap(validDomain -> validateTopLevelDomain.apply(email));
};

// @formatter:on

String email = "john.doe@example.com";
Either<String, String> result4 = validateEmail.apply(email);

assertTrue(result4.isRight());
assertEquals(email, result4.get());

String email2 = "jd@example.com";
Either<String, String> result5 = validateEmail.apply(email2);

assertTrue(result5.isLeft());

Either in other programming languages

Who is using Either?

Result< T>

Result< T > represents a computation that may either result in a value (success) or an exception (failure).

Result examples

//1. Learn to instanciate an Either object.
var resultLeft = Result.failure(new RuntimeException("Katakroker"));
var resultRight = Result.success("Success");

var result2Left = new Failure<>(new RuntimeException("Katakroker"));
var result2Right = new Success<>("Success");

//2. Learn to use Either to not propagate Exceptions any more
Function<String, Result<URI>> toURI = address -> {
    return Result.runCatching(() -> {
        return new URI(address);
    });
};

//3. Process results
Function<Result<URI>, String> process = param -> {
    return switch (param) {
        case Success<URI> success -> success.value().toString();
        case Failure ko -> "";
    };
};

var case1 = "https://www.juanantonio.info";
var result = toURI.andThen(process).apply(case1);
System.out.println("Result: " + result);

// @formatter:off

Function<Result<URI>, String> process2 = param -> {
    return param.fold(
        "",
        onSuccess -> param.getValue().get().toString()
    );
};

// @formatter:on

var case2 = "https://";
var result2 = toURI.andThen(process2).apply(case2);
System.out.println("Result: " + result2);

Result in other programming languages

References

Made with ❤️ from Spain