Try<V>
Runnable example:
TrySample.java
What is Try<V>?
Try<V> models a computation that either succeeds with a value of type V
or fails with a Throwable.
It is a sealed interface with two implementations:
Success<V>— holds the result value (which may benullforTry<Void>).Failure<V>— holds a non-nullThrowable.
Try bridges the gap between exception-throwing code and functional pipelines.
Use it to wrap any computation that throws so exceptions become values you can
map, chain, and recover from — without try/catch blocks scattered through your logic.
The key distinction from Result<V, E> is that the error type is always Throwable.
That makes Try ideal for wrapping legacy or third-party code, but less suitable
when you own the error domain and need callers to branch on specific error subtypes.
Creating instances
// Wrap any throwing computation — catches Throwable, not just ExceptionTry<Integer> parsed = Try.of(() -> Integer.parseInt(input));Try<String> content = Try.of(() -> Files.readString(path));
// Wrap a void side-effect (result value is null on success)Try<Void> written = Try.run(() -> Files.writeString(path, data));
// Construct directly when you already know the outcomeTry<Integer> ok = Try.success(42);Try<Integer> err = Try.failure(new RuntimeException("oops"));Try.of catches any Throwable — not just Exception.
Try.run is the void-safe variant: it yields Success(null) on success.
See the Null handling section for the implications.
Checking state
Try<Integer> t = Try.of(() -> Integer.parseInt(input));
t.isSuccess(); // true if Successt.isFailure(); // true if Failure
// Exhaustive pattern matching — preferredString msg = switch (t) { case Try.Success<Integer> s -> "Parsed: " + s.value(); case Try.Failure<Integer> f -> "Failed: " + f.cause().getMessage();};Prefer exhaustive switch patterns over isSuccess()/isFailure() predicates —
the compiler enforces that both tracks are handled.
Extracting values
| Method | When Failure | Notes |
|---|---|---|
get() | Throws NoSuchElementException | Only safe after an isSuccess() check; prefer alternatives. |
getCause() | N/A — throws if Success | Retrieves the exception; unsafe on the success track. |
getOrElse(fallback) | Returns fallback | Fallback is always evaluated eagerly. |
getOrElseGet(supplier) | Calls supplier | Lazily computed fallback. |
getOrNull() | Returns null | Ambiguous for Try<Void> — avoid when possible. |
getOrThrow() | Rethrows the original exception | Rethrows Exception and Error as-is; wraps bare Throwable. |
getOrThrow(exMapper) | Throws mapped RuntimeException | Maps the cause to a domain exception at the call site. |
fold(onSuccess, onFailure) | Calls onFailure | The most expressive extractor: forces both tracks to be handled. |
Try<Integer> t = Try.of(() -> Integer.parseInt(input));
// Safe: always returns a valueint n = t.getOrElse(0);int m = t.getOrElseGet(() -> computeDefault());
// Expressive: handles both tracksString result = t.fold( value -> "Parsed: " + value, ex -> "Error: " + ex.getMessage());
// Unsafe: throws if Failureint raw = t.get(); // throws NoSuchElementExceptionThrowable e = t.getCause(); // throws NoSuchElementException if Success
// Rethrow the original exception (or wrap it)int val = t.getOrThrow(); // rethrows as-isint v2 = t.getOrThrow(ex -> new AppException(ex)); // maps to RuntimeExceptionTransforming values
map(mapper)
Transforms the success value. The failure track passes through unchanged.
If the mapper itself throws, the exception is captured as a new Failure.
Try<String> binary = Try.of(() -> Integer.parseInt(input)) .map(Integer::toBinaryString); // Try<Integer> → Try<String>
// If the mapper throws, the exception is captured as a FailureTry<JsonNode> json = Try.of(() -> Files.readString(path)) .map(Json::parse); // IOException or JsonException → FailureflatMap(mapper) / flatMapError(mapper)
flatMap chains fallible operations without nesting.
flatMapError operates on the failure track — use it to attempt an alternative
computation when the primary one fails.
// flatMap chains fallible operations without nestingTry<Config> config = Try.of(() -> Files.readString(configPath)) .flatMap(text -> Try.of(() -> Json.parse(text))) .flatMap(json -> Try.of(() -> Config.from(json)));
// Equivalent flatMapError: recover on failure with another TryTry<String> result = Try.of(() -> fetchRemote(url)) .flatMapError(ex -> Try.of(() -> fetchFallback(url)));filter(predicate)
Converts a Success to Failure when the predicate returns false.
The default failure is IllegalArgumentException; the overloads accept a
custom supplier or a value-aware function.
// Default: converts to Failure(IllegalArgumentException) when predicate failsTry<Integer> positive = Try.of(() -> Integer.parseInt(input)) .filter(n -> n > 0);
// Custom exception supplierTry<Integer> valid = Try.of(() -> Integer.parseInt(input)) .filter(n -> n > 0, () -> new IllegalArgumentException("Must be positive"));
// Value-aware error (receives the failing value)Try<Integer> age = Try.of(() -> Integer.parseInt(input)) .filter(n -> n >= 0, n -> new IllegalArgumentException("Negative age: " + n));onSuccess / onFailure
Side-effecting operations that do not alter the Try. Both return this for
chaining. Use match for a terminal void operation that forces both branches.
Try<Integer> t = Try.of(() -> Integer.parseInt(input));
// onSuccess / onFailure — chainable, return thist.onSuccess(n -> log.info("Parsed: {}", n)) .onFailure(ex -> log.warn("Parse failed: {}", ex.getMessage()));
// match — terminal, void; both branches must be handledt.match( value -> metrics.increment("parse.ok"), ex -> metrics.increment("parse.error"));Recovery and fallbacks
Recovery operations convert a Failure back to a Success by inspecting the
exception. Typed overloads match only a specific exception subtype.
// recover — map the exception to a fallback valueTry<String> result = Try.of(() -> fetchRemote(url)) .recover(ex -> "default-value");
// recover by exception type — only handles the matched subtypeTry<String> typed = Try.of(() -> fetchRemote(url)) .recover(IOException.class, ex -> "offline-fallback");
// recoverWith — map to a new Try (for chaining another fallible operation)Try<String> chain = Try.of(() -> fetchPrimary(url)) .recoverWith(ex -> Try.of(() -> fetchSecondary(url)));
// recoverWith by exception typeTry<String> typed2 = Try.of(() -> fetchPrimary(url)) .recoverWith(TimeoutException.class, ex -> Try.of(() -> fetchSecondary(url)));sequence and traverse
Both operations apply fail-fast semantics: they stop at the first Failure
and return it immediately, without consuming the rest of the stream.
This is implemented with Stream.gather(Gatherer.ofSequential(...)) (finalized in Java 24)
to achieve true short-circuit — a design decision documented in ADR-010 — Gatherer for sequence/traverse with true short-circuit.
List<String> inputs = List.of("1", "2", "abc", "4");
// sequence: Stream<Try<V>> → Try<List<V>> — fails fast on first FailureTry<List<Integer>> allOrNone = Try.sequence( inputs.stream().map(s -> Try.of(() -> Integer.parseInt(s))));// → Failure(NumberFormatException) — stops at "abc"
// traverse: applies the mapper and collects — same fail-fast behaviourTry<List<Integer>> result = Try.traverse(inputs, s -> Try.of(() -> Integer.parseInt(s)));
// sequence also accepts an IterableList<Try<Integer>> tries = List.of(Try.success(1), Try.success(2));Try<List<Integer>> list = Try.sequence(tries); // → Success([1, 2])Stream collectors
Try.toList() and Try.partitioningBy() are stream Collector counterparts to
sequence — they operate on a Stream<Try<V>> instead of a list.
| Method | Returns | Semantics |
|---|---|---|
Try.toList() | Try<List<V>> | Returns first Failure (upstream fully traversed) |
Try.partitioningBy() | Try.Partition<V> | Collect all — splits successes() and failures() |
Try.Partition<V> is a record with two unmodifiable lists.
Important: toList() always consumes the entire stream — the Collector contract has no early-termination mechanism. Use sequence when you need true fail-fast behaviour on large or expensive streams.
The intentional difference between toList() and sequence is documented in ADR-016 — Try.toList() collector consumes the entire stream while sequence is fail-fast.
// ── Try.toList() — returns first Failure (stream still fully traversed) ──────Try<List<Integer>> parsed = Stream.of("1", "2", "3") .map(s -> Try.of(() -> Integer.parseInt(s))) .collect(Try.toList());// → Success([1, 2, 3])
Try<List<Integer>> broken = Stream.of("1", "bad", "3") .map(s -> Try.of(() -> Integer.parseInt(s))) .collect(Try.toList());// → Failure(NumberFormatException) — "3" is still parsed; collector returns first Failure
// Real-world: parse all config values — returns first parse error encountered// Note: all keys are parsed regardless; do not rely on early termination for// expensive or side-effecting upstream work.Try<List<Duration>> timeouts = configKeys.stream() .map(key -> Try.of(() -> Duration.parse(config.get(key)))) .collect(Try.toList());
// ── Try.partitioningBy() — process all, split successes from failures ─────────Try.Partition<Path> result = filePaths.stream() .map(path -> Try.of(() -> validateFile(path))) .collect(Try.partitioningBy());
List<Path> valid = result.successes(); // files that passed validationList<Throwable> errors = result.failures(); // exceptions from invalid files
// Real-world: import as many files as possible; log failures without abortingimportService.bulkProcess(result.successes());result.failures().forEach(ex -> log.error("Skipping file: {}", ex.getMessage()));Collector facade: Tries
Tries is a single discoverable entry point for all Stream<Try<V>> collectors —
analogous to java.util.stream.Collectors. Import dmx.fun.Tries instead of
remembering which collector lives on which type.
The decision to provide companion facade classes as pure-delegation entry points is documented in ADR-014 — Facade collectors (Results, Options, Tries) as a single entry point.
| Method | Returns | Semantics |
|---|---|---|
Tries.toList() | Try<List<V>> | Delegates to Try.toList() |
Tries.partitioning() | Tries.Partition<V> | Delegates to Try.partitioningBy() |
Tries.Partition is a thin wrapper that delegates to Try.Partition.
Call toTryPartition() to obtain the underlying Try.Partition<V> when needed.
// ── Tries.toList() — same as Try.toList(), single import entry point ─────────Try<List<Integer>> all = Stream.of("10", "20", "30") .map(s -> Try.of(() -> Integer.parseInt(s))) .collect(Tries.toList());// → Success([10, 20, 30])
// ── Tries.partitioning() — collect all results, split by outcome ─────────────Tries.Partition<byte[]> partition = urls.stream() .map(url -> Try.of(() -> httpClient.fetch(url))) .collect(Tries.partitioning());
partition.successes().forEach(body -> indexer.ingest(body));partition.failures().forEach(ex -> metrics.recordError(ex));
// Bridge to Try.Partition when neededTry.Partition<byte[]> tp = partition.toTryPartition();Null handling
Try<Void> is a special case produced by Try.run(). Its success value is
null. This has one important consequence: toOption() always returns None
for a Try<Void>, even on success — use isSuccess() or fold() instead.
sequence, traverse, toList(), and Try.Partition all use
Collections.unmodifiableList internally (not List.copyOf),
so null elements in successful results are preserved.
This implementation choice is documented in ADR-009 — unmodifiableList instead of List.copyOf in Try.
This asymmetry with Result.Ok — which rejects null — is a deliberate design decision
documented in ADR-004 — Try allows Success(null); Result.Ok rejects null.
// Try.run() wraps a void computation — the success value is nullTry<Void> written = Try.run(() -> Files.writeString(path, content));written.isSuccess(); // truewritten.getOrNull(); // null — do NOT use toOption() here; it always returns None
// Use isSuccess() or fold() to observe a run() outcomeboolean ok = written.isSuccess();String msg = written.fold(_ -> "written", ex -> "failed: " + ex.getMessage());
// sequence preserves null values (uses Collections.unmodifiableList, not List.copyOf)Try<List<String>> result = Try.sequence( Stream.of(Try.success("a"), Try.success(null), Try.success("c")));// → Success(["a", null, "c"])Interoperability
| Conversion | Method | Notes |
|---|---|---|
Try → Option<V> | tryVal.toOption() | Failure and Success(null) both → None |
Try → Result<V, Throwable> | tryVal.toResult() | Keeps the exception as a typed error |
Try → Result<V, E> | tryVal.toResult(errorMapper) | Maps exception to a domain error type |
Try → Either<Throwable, V> | tryVal.toEither() | Throws NullPointerException on Success(null) |
Try → Optional<V> | tryVal.toOptional() | Failure and Success(null) both → Optional.empty() |
Try → Stream<V> | tryVal.stream() | Single-element on Success, empty on Failure |
Try → CompletableFuture<V> | tryVal.toFuture() | Already-completed future |
Option → Try | Try.fromOption(opt, exceptionSupplier) | None → Failure(supplied exception) |
Optional → Try | Try.fromOptional(optional, exceptionSupplier) | empty → Failure(supplied exception) |
Either<L, R> → Try | Try.fromEither(either, leftMapper) | Accepts Either<? extends L, ? extends R> (covariant); left(l) → Failure(leftMapper(l)) |
Result<V, Throwable> → Try | Try.fromResult(result) | Accepts Result<? extends V, ? extends Throwable> (covariant) |
Result<V, E> → Try | result.toTry(errorMapper) | Maps error to a Throwable |
CompletableFuture<V> → Try | Try.fromFuture(future) | Blocks; unwraps CompletionException |
Try<String> t = Try.of(() -> Files.readString(path));
// Try → Option (discards the exception; Success(null) also maps to None)Option<String> opt = t.toOption();
// Try → Result<V, Throwable> (keeps the exception as a typed error)Result<String, Throwable> raw = t.toResult();
// Try → Result<V, E> with error mappingResult<String, AppError> typed = t.toResult(ex -> new AppError(ex.getMessage()));
// Try → Either<Throwable, V> (throws NPE on Success(null))Either<Throwable, String> either = t.toEither();
// Try → Optional<V> (Failure and Success(null) → Optional.empty())Optional<String> javaOpt = t.toOptional();
// Try → Stream<V> (empty on Failure, single element on Success)t.stream().forEach(System.out::println);
// Try → CompletableFuture<V> (already-completed future)CompletableFuture<String> future = t.toFuture();
// ── From other types ──────────────────────────────────────────────────────────
// Option → TryOption<String> option = Option.some("hello");Try<String> fromOpt = Try.fromOption(option, () -> new NoSuchElementException("missing"));
// Optional → TryOptional<String> optional = Optional.of("hello");Try<String> fromOptional = Try.fromOptional(optional, () -> new NoSuchElementException("missing"));
// Either<L, R> → Try (left value mapped to a Throwable)Either<String, Integer> left = Either.left("not found");Either<String, Integer> right = Either.right(42);Try<Integer> fromLeft = Try.fromEither(left, IllegalArgumentException::new);// Try.failure(new IllegalArgumentException("not found"))Try<Integer> fromRight = Try.fromEither(right, IllegalArgumentException::new);// Try.success(42)
// Result<V, Throwable> → TryResult<String, IOException> result = Result.err(new IOException("oops"));Try<String> fromResult = Try.fromResult(result);
// CompletableFuture → Try (blocks until complete; unwraps CompletionException)Try<String> fromFuture = Try.fromFuture(CompletableFuture.completedFuture("done"));See the Combining Types page for the full conversion matrix and composition patterns.
Try vs Result vs checked exceptions
| Checked exceptions | Try<V> | Result<V, E> | |
|---|---|---|---|
| Error type | Declared throws clause | Always Throwable | Any domain type |
| Forces handling | Yes — compile time | Yes — via API | Yes — via API and switch patterns |
| Branch on error subtype | Via catch clauses | Via instanceof / recover(Class) | Via sealed subtype and switch patterns |
| Interop with throwing code | Native | Excellent — Try.of(() -> ...) | Indirect — wrap with Try first |
| Best for | Internal APIs you control | Wrapping legacy/third-party code | Domain logic with typed failure modes |
Time-bounded execution — Try.withTimeout
Try.withTimeout(Duration, CheckedSupplier) runs the supplier on a virtual thread
and enforces the time limit via Future.get(timeout, unit).
- If the computation completes in time →
Success(value). - If the deadline is exceeded → the virtual thread is interrupted and a
Failure(TimeoutException("Operation timed out after Xms"))is returned. - If the computation throws before the deadline →
Failure(originalCause).
Requires Java 21+ (virtual threads). Because Try is an immutable value type
(already computed), timeout applies at creation time — use the static factory
rather than trying to retroactively time-limit an existing Try.
The choice to use Thread.ofVirtual() over a platform-thread ExecutorService or a shared
CompletableFuture pool is documented in ADR-013 — Try.withTimeout uses virtual threads (Thread.ofVirtual()).
// Static factory — enforce a time limit from the startTry<Response> result = Try.withTimeout( Duration.ofSeconds(5), () -> httpClient.get(url));
// Exceeded deadline → Failure(TimeoutException("Operation timed out after 100000000ns"))Try<String> timedOut = Try.withTimeout( Duration.ofMillis(100), () -> { Thread.sleep(10_000); return "never"; });timedOut.isFailure(); // truetimedOut.getCause(); // TimeoutException: "Operation timed out after 100000000ns"
// Composes naturally with the rest of the APITry.withTimeout(Duration.ofSeconds(3), () -> fetchUser(id)) .recover(TimeoutException.class, ex -> User.anonymous()) .flatMap(user -> validateActive(user)) .onSuccess(user -> cache.put(id, user));Common pitfalls
Calling get() without checking state
get() throws NoSuchElementException on Failure. Use getOrElse,
fold, or pattern matching instead.
// Bad: throws NoSuchElementException if Failureint n = Try.of(() -> Integer.parseInt(input)).get();
// Good: safe extractionint n = Try.of(() -> Integer.parseInt(input)).getOrElse(0);
// Good: handle both tracks explicitlyint n = Try.of(() -> Integer.parseInt(input)) .fold(value -> value, ex -> { log.warn("bad input", ex); return 0; });Using Try when Result is the right tool
Try discards the error domain — callers can only catch Throwable.
If you own the error type and callers need to branch on specific subtypes,
use Result. Reserve Try for the boundary where third-party code throws.
// Bad: Try buries the error domain — callers can't branch on error typeTry<User> findUser(long id) { return Try.of(() -> userRepo.find(id));}
// Good: when you own the error type, use ResultResult<User, UserError> findUser(long id) { return userRepo.find(id) .map(Result::<User, UserError>ok) .orElse(Result.err(UserError.NOT_FOUND));}
// Good: use Try only at the boundary where legacy code throwsResult<User, UserError> findUser(long id) { return Try.of(() -> legacyRepo.find(id)) .toResult(ex -> new UserError(ex.getMessage()));}Using toOption() after Try.run()
Try.run() produces Success(null). Converting to Option via toOption()
always returns None — the null success value is silently discarded.
Use isSuccess() or fold() to observe the outcome of a run() call.
Real-world example
A file-read → parse → validate → save pipeline. Each step may throw;
Try captures the exception at every boundary without nested try/catch blocks.
// File → parse → validate → save pipeline// Each step may throw; Try captures the exception at every boundary.
Try<ReportId> saveReport(Path path) { return Try.of(() -> Files.readString(path)) // IOException .map(text -> Json.parse(text)) // JsonException .flatMap(json -> Try.of(() -> validate(json))) // ValidationException .flatMap(valid -> Try.of(() -> reportRepo.save(valid))) // SQLException .map(Report::id);}
// At the call site, fold handles both outcomessaveReport(uploadedPath).fold( id -> Response.ok("Saved report " + id), ex -> switch (ex) { case IOException e -> Response.error(503, "Storage unavailable"); case JsonException e -> Response.error(400, "Invalid JSON"); case ValidationException e -> Response.error(422, e.getMessage()); default -> Response.error(500, "Unexpected error"); });Every step propagates its Failure automatically.
At the call site, fold with a pattern-matched switch handles each exception
type individually.