Back to Stream: Exception Handling
Q4Question 4 of 4

How do you collect BOTH successful results and errors from a stream pipeline?

Key Concepts
  • Map each element to a Result object holding either a success value or an error, not throwing
  • partitioningBy(Result::isSuccess) splits results into successes and failures in one pass
  • One bad element does not abort the pipeline: every element is processed and classified
  • Failures carry enough context (original input, exception message) for reporting or retry
  • This is the functional alternative to exceptions for expected per-element failures in batch processing
  • For truly exceptional unrecoverable errors, still throw: result-objects are for expected failures

Definition

Map each element to a success-or-error result object, then partition into successes and failures so one bad element does not abort the whole pipeline.
  • Result object holds either a success value or an error (Either pattern).
  • Mapping step runs the risky operation in try-catch, returning success or failure.
  • partitioningBy(isSuccess) splits into two groups: successes and failures.
  • Partial success: every element is processed; bad ones do not abort the pipeline.
  • Use case: batch processing where some records are malformed but the rest should proceed.

At National Bank of India, Priya processes each transaction form. Instead of stopping at the first bad form, she stamps every form as either 'done' or 'failed', then sorts them into a done pile and a failed pile. The good ones proceed, and the bad ones go to the error report.

Understanding the Concepts

Why not just throw?

In a stream pipeline, throwing on the first bad element aborts the entire pipeline. For batch processing with thousands of records, this means losing all progress because of one malformed entry. The result-object approach processes every element and classifies each as success or failure.

How do you implement it?

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

public class CollectSuccessesAndErrors {

    record Result<T>(T value, String error) {
        boolean isSuccess() { return error == null; }
        static <T> Result<T> success(T value) { return new Result<>(value, null); }
        static <T> Result<T> failure(String error) { return new Result<>(null, error); }
    }

    static Result<BigDecimal> parseAmount(String raw) {
        try {
            return Result.success(new BigDecimal(raw));
        } catch (NumberFormatException e) {
            return Result.failure("Invalid amount: " + raw);
        }
    }

    public static void main(String[] args) {
        List<String> rawAmounts = List.of(
            "1500000", "invalid", "900000", "abc", "1200000");

        Map<Boolean, List<Result<BigDecimal>>> partitioned = rawAmounts.stream()
            .map(CollectSuccessesAndErrors::parseAmount)
            .collect(Collectors.partitioningBy(Result::isSuccess));

        List<BigDecimal> successes = partitioned.get(true).stream()
            .map(Result::value)
            .toList();

        List<String> errors = partitioned.get(false).stream()
            .map(Result::error)
            .toList();

        System.out.println("Successes: " + successes);
        System.out.println("Errors: " + errors);
        System.out.printf("Processed %d of %d successfully%n",
            successes.size(), rawAmounts.size());
    }
}
TEXT
Successes: [1500000, 900000, 1200000]
Errors: [Invalid amount: invalid, Invalid amount: abc]
Processed 3 of 5 successfully

How does the Result type work?

| Factory | Value | Error | isSuccess |
|---|---|---|---|
| Result.success(value) | Present | null | true |
| Result.failure(error) | null | Present | false |

What about using Optional instead of a custom Result?

JAVA
// Simpler but loses the error message
List<Optional<BigDecimal>> results = rawAmounts.stream()
    .map(s -> {
        try { return Optional.of(new BigDecimal(s)); }
        catch (NumberFormatException e) { return Optional.<BigDecimal>empty(); }
    })
    .toList();

// Keep only successes
List<BigDecimal> values = results.stream()
    .flatMap(Optional::stream)
    .toList();
// Downside: no information about which inputs failed or why

How is the Either/Result pattern different from throwing?

| Aspect | Throwing | Result/Either |
|---|---|---|
| First error | Aborts pipeline | Continues processing |
| Error info | Exception message + stack trace | Stored in result object |
| Partial results | Lost | Preserved |
| Use case | Exceptional, unrecoverable | Expected, per-element |

What are common mistakes?

Mistake: Using exceptions for expected per-element failures in batch processing.

JAVA
// WRONG: first bad record aborts the entire batch
rawAmounts.stream()
    .map(BigDecimal::new)  // throws NumberFormatException on "invalid"
    .toList();              // aborts, all previous results lost

// CORRECT: result-object approach processes all records
rawAmounts.stream()
    .map(CollectSuccessesAndErrors::parseAmount)
    .collect(Collectors.partitioningBy(Result::isSuccess));

What about using vavr's Either or Try?

Libraries like vavr provide production-grade Either and Try types with rich APIs. For production code, consider using them instead of a hand-rolled Result.

What should a senior engineer know?

This is the functional alternative to exceptions for expected failures. It keeps the pipeline total (every input produces an output) and lets you aggregate errors. The decision between result-objects and exceptions is about whether partial success matters for the task.

What are common interview follow-up questions?

  • "When should you throw vs use Result?" Throw for unrecoverable errors; use Result for expected per-element failures.
  • "How do you retry failed records?" Collect the failure inputs and feed them through a retry pipeline.
  • "What library provides Either?" vavr (previously javaslang).

Visual Overview

DIAGRAM
  Input: ["1500000", "invalid", "900000", "abc", "1200000"]
       │
       │  map(parseAmount)
       ▼
  [Success(1.5M), Failure(invalid), Success(0.9M),
   Failure(abc), Success(1.2M)]
       │
       │  partitioningBy(isSuccess)
       ▼
  ┌──────────────────────────────────────┐
  │  true  → [1.5M, 0.9M, 1.2M]        │  ← successes
  │  false → [invalid, abc]             │  ← errors
  └──────────────────────────────────────┘

Key Takeaway

To collect both successes and errors, stop throwing and model each element's outcome as a result object holding either a success value or an error. The mapping step runs the risky operation in try-catch and returns a success or failure result. Then partitioningBy(isSuccess) splits the stream into the two groups.

At the National Bank of India, Priya uses this for a batch that parses transaction amounts where some records are malformed. The job processes all valid records and collects the invalid ones for an error report, instead of failing entirely on the first bad record.

This is the functional alternative to exceptions for expected per-element failures. It keeps the pipeline total (every input produces an output) and lets you aggregate errors rather than losing all progress. For truly exceptional, unrecoverable errors, still throw.

Remember one sentence: map each element to a success-or-failure result object and partition, so one bad element does not abort the whole batch.

---

Share
Use ← → arrow keys to navigate