Common Compiler Errors
Java's type inference works well for most Effect Path usage, but generic-heavy code occasionally produces confusing messages. This page covers the five things that most often go wrong. Find your symptom in the table below and follow it to its section.
- Which Path mistakes javac catches, which the HKJ checker catches, and which nothing catches
- How to pin the error type
Eso it never silently becomesObject - How to convert at the boundary when a step returns the wrong kind of Path
- When to reach for
maprather thanvia, and how to unblock a lambda javac cannot infer
The compiler is not your safety net on this page.
- §3 and §4 are javac errors. The build stops, and the message is quoted in the section.
- §2 and §5 compile. The HKJ compile-time checker reports them if you use the build plugin:
path-type-mismatchas an error,error-type-mismatchas a warning. Without the plugin the first sign is anIllegalArgumentExceptionor aClassCastExceptionin production. See Compile-Time Checks for setup and the full catalogue. - §1 compiles, and nothing reports it.
EbecomesObjectand stays there.
Each silent case carries a snippet the book's build compiles, asserting that it still compiles. A change that makes one of them loud again fails that build rather than passing in silence.
Find your message
| The symptom | What it is | Caught by |
|---|---|---|
Path.right/left silently gets E = Object | E is unconstrained, so javac defaults it | Nothing. Add the witness yourself |
IllegalArgumentException from via, with no compile error | A step returns a different Path kind | The path-type-mismatch check |
method via is not applicable | The function returns a plain value, not a Path | javac |
lambda body is neither value nor void compatible | A lambda's branches return different types | javac |
| Wrong error type at runtime, with no compile error | E is erased across a chain step | The error-type-mismatch check, as a warning |
Which line of defence answers depends on the mistake:
flowchart TD
C["Your Path chain"]
J{"Does javac<br/>reject it?"}
JE["Build stops<br/>§3, §4"]
K{"Does the HKJ<br/>checker report it?"}
KE["Reported at compile time<br/>§2 as an error, §5 as a warning"]
N["Nothing says a word<br/>§1, and §2 or §5 without the plugin"]
C --> J
J -->|yes| JE
J -->|no| K
K -->|yes| KE
K -->|no| N
classDef step fill:#8caaee,stroke:#1e66f5,color:#232634
classDef decision fill:#e5c890,stroke:#df8e1d,color:#232634
classDef caught fill:#a6d189,stroke:#40a02b,color:#232634
classDef missed fill:#e78284,stroke:#d20f39,color:#232634
class C step
class J,K decision
class JE,KE caught
class N missed
1. The phantom error type E on Path.right(...)
This compiles, and nothing reports it. Path.right(value) carries E only in its return type. When the context constrains E, javac binds it; when nothing does, javac resolves E to java.lang.Object and says nothing.
The trigger:
EitherPath<AppError, User> findUser(String id) {
User user = repository.findById(id);
return Path.right(user); // E bound to AppError by the return type: fine
}
var p = Path.right(user); // nothing constrains E, so E = Object, and it compiles
The fix: pin E explicitly, so the intent is recorded and Object never leaks in:
EitherPath<AppError, User> pinned = Path.<AppError, User>right(user);
When the witness matters
When the witness matters
With a clear target type the witness is optional:
EitherPath<AppError, User> path = Path.right(user); // E = AppError, from the variable
EitherPath<AppError, User> chained =
Path.<AppError, String>right(userId)
.via(id -> Path.right(loadUser(id))); // E from the previous step
Without one, E becomes Object silently:
var path = Path.right(user); // E = Object: add the witness
The witness costs nothing at runtime and keeps the error type honest.
Why it is silent, and what older write-ups said
Why it is silent, and what older write-ups said
Older write-ups described a cannot infer type-variable(s) E error here:
error: cannot infer type arguments for right(A)
reason: cannot infer type-variable(s) E
On the supported compiler, the modern javac the HKJ build plugin targets, this does not happen. The code compiles. That is the more dangerous outcome, because the mistake is now silent rather than loud.
When E defaults to Object, later code expecting a specific error type either fails to type-check at the consumer, as an ordinary incompatible-types error somewhere else, or has its real error type erased inside a chain. See §5.
The HKJ compile-time checker does not flag the bare E = Object default, because it is not reliably distinguishable from code that means EitherPath<Object, …>. It does flag the related hazard in §5, through the error-type-mismatch check.
2. A step that returns the wrong kind of Path
This compiles, and throws at runtime. via takes a Function<? super A, ? extends Chainable<B>>, and every Path type is a Chainable, so which Path a step returns is not part of the signature. via checks the kind at runtime and throws IllegalArgumentException.
The trigger:
MaybePath<User> findUser(String id) {
return Path.maybe(loadUser(id));
}
EitherPath<AppError, String> result =
Path.<AppError, String>right(userId)
.via(id -> findUser(id)) // returns MaybePath, not EitherPath
.map(User::name);
The fix: convert at the boundary with toEitherPath, which turns Nothing into a Left. A lambda selects the deferred overload, so the error is built only on the branch that uses it, and the <AppError> witness names the error type, since nothing downstream settles it here.
EitherPath<AppError, String> result =
Path.<AppError, String>right(userId)
.via(id -> Path.maybe(loadUser(id))
.<AppError>toEitherPath(() -> new AppError.UserNotFound(id))) // MaybePath -> EitherPath
.map(User::name);
Common conversions
| From | To | Method |
|---|---|---|
MaybePath<A> | EitherPath<E, A> | .toEitherPath(errorValue), .toEitherPath(errorSupplier) |
TryPath<A> | EitherPath<E, A> | .toEitherPath(exceptionMapper) |
EitherPath<E, A> | MaybePath<A> | .toMaybePath() |
ValidationPath<E, A> | EitherPath<E, A> | .toEitherPath() |
With the build plugin, the path-type-mismatch check reports it at compile time, with an actionable message at the call site. See Compile-Time Checks.
What older write-ups said
What older write-ups said
Older write-ups described javac rejecting this:
error: incompatible types: MaybePath<User> cannot be converted to EitherPath<AppError,User>
.via(id -> Path.maybe(findUser(id)))
^
It does not. An EitherPath chain does expect via to return an EitherPath, but nothing in the signature says so, which is why the mistake reaches runtime.
3. "Method via is not applicable for the arguments"
A javac error. via is the Effect Path equivalent of flatMap, so it requires a function returning a Chainable, which every Path type implements. A function returning a plain value needs map.
The error:
error: method via in class EitherPath<E,A> cannot be applied to given types;
.via(this::processOrder)
^
required: Function<? super Order, ? extends Chainable<B>>
found: method reference this::processOrder
The trigger:
// processOrder returns the wrong type
String processOrder(Order order) { // returns String, not a Path
return order.id();
}
EitherPath<AppError, String> chained =
Path.<AppError, Order>right(order)
.via(this::processOrder); // via needs a Path-returning function
The fix: use map for plain transformations and via for Path-returning functions.
// For plain transformations: use map
EitherPath<AppError, String> mapped =
Path.<AppError, Order>right(order)
.map(this::processOrder); // map: A -> B
// For Path-returning functions: use via
EitherPath<AppError, Order> chained =
Path.<AppError, Order>right(order)
.via(this::validateAndProcessOrder); // via: A -> Path<B>
Rule of thumb:
map: your function takesAand returnsBvia: your function takesAand returns aPath<B>, of any Path type that matches the chain
The via-non-path check is the companion to javac's own error, and carries the actionable "use map for a plain transformation" message. See Compile-Time Checks.
4. "Lambda body is neither value nor void compatible"
A javac error. A lambda whose branches do not all return a value of the same type gives javac nothing to infer B from.
The error:
error: lambda body is neither value nor void compatible
.map(o -> {
^
error: method map in class EitherPath<E,A> cannot be applied to given types;
required: Function<? super Order,? extends B>
reason: cannot infer type-variable(s) B
(argument mismatch; bad return type in lambda expression
missing return value)
The trigger:
EitherPath<AppError, Double> total =
Path.<AppError, Order>right(order)
.map(o -> {
if (o.isValid()) {
return o.total(); // returns Double
}
// missing return: compiler cannot determine B
});
The fix: make every branch return the same type, or give the parameter an explicit type.
// Fix 1: ensure all branches return the same type
EitherPath<AppError, Double> total =
Path.<AppError, Order>right(order)
.map(o -> {
if (o.isValid()) {
return o.total();
}
return 0.0; // all branches return Double
});
// Fix 2: add explicit types when inference fails
EitherPath<AppError, Double> explicit =
Path.<AppError, Order>right(order)
.map((Order o) -> o.total());
Two things bring this on: a lambda with branches of different return types, or one branch missing altogether, and a lambda parameter whose type cannot be inferred in a complex chain. In long chains where inference struggles, lifting the lambda into a named method usually settles it:
private Double extractTotal(Order order) {
return order.isValid() ? order.total() : 0.0;
}
// Method reference: no inference needed
EitherPath<AppError, Double> total =
Path.<AppError, Order>right(order)
.map(this::extractTotal);
5. The error type is silently erased across a chain
This compiles, and that is the bug. via, flatMap and then accept a Function or Supplier of ? extends Chainable<B>, and zipWith a Combinable<B>. None of those carries the error type. A step whose E differs from the chain's compiles cleanly, and the wrong error type is carried at runtime, surfacing as a ClassCastException when the error is consumed.
The trigger:
// lookupUser returns EitherPath<String, User>, which is the wrong error type
EitherPath<String, User> lookupUser(String id) {
return id.isEmpty()
? Path.<String, User>left("User not found") // error type is String
: Path.<String, User>right(new User(id));
}
EitherPath<AppError, String> validated = validateInput(input);
// String erased; the result is typed AppError, and is not
EitherPath<AppError, User> wrong = validated.via(id -> lookupUser(id));
The fix: unify the error type, or convert it with mapError.
// Option 1: change lookupUser to use AppError
EitherPath<AppError, User> lookupUser(String id) {
return id.isEmpty()
? Path.<AppError, User>left(new AppError.NotFound("User not found"))
: Path.<AppError, User>right(new User(id));
}
// Option 2: convert the error type at the boundary
EitherPath<String, User> lookupByName(String id) {
return Path.<String, User>right(new User(id));
}
EitherPath<AppError, String> validated = validateInput(input);
EitherPath<AppError, User> converted =
validated.via(id -> lookupByName(id)
.mapError(msg -> new AppError.NotFound(msg))); // String -> AppError
Option 1 is preferred for new code. Option 2 is useful when integrating with existing methods you cannot change.
The error-type-mismatch check reports the silent mismatch, as a warning by default, since the compiler itself accepts the code. It fires when the receiver and the step are the same error-typed Path category and the step's E is not assignable to the chain's. See Compile-Time Checks.
What older write-ups said
What older write-ups said
This case was previously documented as a compile error:
error: incompatible types: EitherPath<String,User> cannot be converted to
EitherPath<AppError,User>
On the supported compiler it is not an error. Every step in an EitherPath chain is meant to share one error type E, but the chain signatures erase it through Chainable<B>, so the compiler will not enforce it for you.
- Only two of the five are javac errors. For the rest, the build plugin's compile-time checks are what stands between you and a production exception.
- Pin
Eat the source.Path.<E, A>right(...)costs nothing at runtime and stopsObjectleaking into an error type nothing will notice. viadoes not check which Path you hand back. Every Path is aChainable, so convert at the boundary withtoEitherPathortoMaybePathrather than trusting the signature.maptransforms,viachains. A function returning a plain value belongs inmap; one returning a Path belongs invia.- A chain's error type is a convention, not a constraint. Unify
Eacross steps, or convert withmapErrorwhere you cannot.
- Compile-Time Checks: the checks that report the silent cases, with setup and configuration
- Optics Compiler Errors: what
@GeneratePathBridgeand@PathViareport, and why - Type Conversions: full reference for converting between Path types
- Troubleshooting: tutorial-specific issues (Kind types, annotation processors, IDE setup)
- Cheat Sheet: quick reference for Path types and operators
Previous: Type Conversions Next: Production Readiness