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 as-is; wraps Error/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.
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])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 uses Collections.unmodifiableList internally (not List.copyOf),
so null elements in successful results are preserved.
// 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 |
|---|---|
Try → Option | tryVal.toOption() |
Try → Result<V, Throwable> | tryVal.toResult() |
Try → Result<V, E> | tryVal.toResult(errorMapper) |
Try → Either<Throwable, V> | tryVal.toEither() |
Try → Stream<V> | tryVal.stream() |
Option → Try | option.toTry(exceptionSupplier) |
Result → Try | result.toTry(errorMapper) |
Try<String> t = Try.of(() -> Files.readString(path));
// Try → Option (discards the exception)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>Either<Throwable, String> either = t.toEither();
// Try → Stream<V> (empty on Failure, single element on Success)t.stream().forEach(System.out::println);
// Option → TryOption<String> option = Option.some("hello");Try<String> fromOpt = option.toTry(() -> new NoSuchElementException("missing"));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.
// 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.