Claude-skill-registry java-error-as-value
Implement error-as-value pattern in Java using Result type for type-safe error handling. Use this skill when implementing error handling without exceptions, converting try-catch to Result pattern, or designing type-safe error handling in Java applications. Particularly useful for functional programming style, API design, and when errors are expected and recoverable.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/java-error-as-value" ~/.claude/skills/majiayu000-claude-skill-registry-java-error-as-value && rm -rf "$T"
skills/data/java-error-as-value/SKILL.mdJava Error-as-Value Pattern
Overview
This skill enables error-as-value pattern in Java using the Result<T, E> type, an alternative to exception-based error handling. The Result type represents either success (Ok) or failure (Err), making errors explicit in the type system and enabling functional composition of fallible operations.
Inspired by Rust's Result type and Haskell's Either type, this pattern provides:
- Type-safe error handling: Errors are values in the type system
- Explicit error flow: No hidden control flow via exceptions
- Composable operations: Chain fallible operations with map, andThen, etc.
- No stack unwinding overhead: Better performance for expected errors
When to Use This Skill
Invoke this skill when:
-
Implementing new error-handling code where errors are expected and recoverable
- Examples: "Parse this user input using Result pattern", "Add validation with error-as-value"
-
Refactoring exception-based code to Result pattern
- Examples: "Convert this try-catch to Result", "Refactor this method to return Result instead of throwing"
-
Designing APIs that make error handling explicit
- Examples: "Design a configuration parser using Result", "Create a validation pipeline with Result"
-
Working with functional-style code that chains operations
- Examples: "Chain these database operations using Result", "Compose these validation steps"
-
User explicitly mentions error-as-value, Result type, or functional error handling
- Examples: "Use error-as-value pattern", "Implement this with Result<T, E>"
Quick Start
1. Copy the Result Type
The Result<T, E> implementation is provided in
assets/Result.java. Copy this file to the project:
// Copy assets/Result.java to src/main/java/Result.java // or appropriate package directory
2. Define Error Types
Create domain-specific error enums or sealed interfaces:
// Simple enum errors (recommended for most cases) enum ValidationError { INVALID_EMAIL, WEAK_PASSWORD, USERNAME_TAKEN } // Rich sealed interface errors (for complex error data) sealed interface ConfigError { record FileNotFound(String path) implements ConfigError {} record InvalidJson(String reason) implements ConfigError {} record MissingField(String field) implements ConfigError {} }
3. Return Result from Methods
Replace exception-throwing methods with Result-returning methods:
// Before: Exception-based public User getUser(String id) throws UserNotFoundException { // ... } // After: Result-based public Result<User, UserError> getUser(String id) { if (!userExists(id)) { return Result.err(UserError.NOT_FOUND); } return Result.ok(fetchUser(id)); }
4. Handle Results at Call Sites
Use pattern matching or combinators to handle results:
// Pattern matching String message = getUser(id).match( user -> "Found: " + user.getName(), error -> "Error: " + error ); // Chaining operations Result<String, UserError> userName = getUser(id) .map(user -> user.getName()) .mapErr(error -> handleError(error)); // Side effects getUser(id) .ifOk(user -> processUser(user)) .ifErr(error -> logError(error));
Core Capabilities
1. Generate Result Boilerplate
When asked to implement error-as-value pattern, copy the Result<T, E> class from
assets/Result.java to the appropriate location in the project.
The Result type provides:
andok(value)
- constructorserr(error)
andisOk()
- type checkingisErr()
,unwrap()
,unwrapOr()
- value extractionunwrapOrElse()
,map()
- transformationsmapErr()
- monadic chaining (flatMap)andThen()
,ifOk()
- side effectsifErr()
- pattern matchingmatch()
- conversion to OptionaltoOptional()
- exception wrappingof()
2. Design Error Types
When implementing error handling, design appropriate error types based on the use case:
Use String errors for prototyping or simple cases:
Result<Integer, String> parse(String input) { return Result.of( () -> Integer.parseInt(input), e -> "Invalid number: " + e.getMessage() ); }
Use enum errors for production code with discrete error cases:
enum ParseError { INVALID_FORMAT, OUT_OF_RANGE, EMPTY_INPUT } Result<Integer, ParseError> parseAge(String input) { if (input.isEmpty()) { return Result.err(ParseError.EMPTY_INPUT); } // ... }
Use sealed interface errors when errors need to carry data:
sealed interface ValidationError { record Required(String field) implements ValidationError {} record TooShort(String field, int min, int actual) implements ValidationError {} record InvalidFormat(String field, String pattern) implements ValidationError {} }
See
references/guidelines.md for detailed error type design guidance.
3. Convert Exception-Based Code to Result
When refactoring try-catch blocks to Result pattern:
Step 1: Identify the error types that can be thrown Step 2: Create an appropriate error enum or sealed interface Step 3: Use
Result.of() to wrap exception-throwing code
Step 4: Update the method signature to return Result
Step 5: Update all call sites to handle Result
Example transformation:
// Before public Config loadConfig(String path) throws IOException, ParseException { String content = Files.readString(Paths.get(path)); return parseConfig(content); } // After public Result<Config, ConfigError> loadConfig(String path) { return Result.of( () -> Files.readString(Paths.get(path)), e -> new ConfigError.FileNotFound(path) ).andThen(content -> parseConfig(content)); }
4. Chain Fallible Operations
Use
andThen() for operations that return Result (monadic chaining):
Result<Order, String> result = validateCart(cart) .andThen(validCart -> calculateTotal(validCart)) .andThen(total -> createOrder(cart, total)) .andThen(order -> saveToDatabase(order)) .andThen(order -> sendConfirmation(order)); // First error short-circuits the chain
Use
map() for transforming success values:
Result<String, Error> upperName = getUser(id) .map(user -> user.getName()) .map(name -> name.toUpperCase());
5. Implement Validation Pipelines
For multi-field validation, accumulate errors or fail fast:
// Fail fast (stop at first error) Result<UserInput, ValidationError> result = validateUsername(input.username()) .andThen(u -> validateEmail(input.email())) .andThen(e -> validatePassword(input.password())) .map(p -> input); // Accumulate all errors public Result<UserInput, List<ValidationError>> validate(UserInput input) { List<ValidationError> errors = new ArrayList<>(); validateUsername(input.username()).ifErr(errors::add); validateEmail(input.email()).ifErr(errors::add); validatePassword(input.password()).ifErr(errors::add); if (errors.isEmpty()) { return Result.ok(input); } return Result.err(errors); }
6. Generate Practical Examples
When requested to demonstrate the pattern, use realistic scenarios from
references/examples.md:
- User registration with validation and database operations
- Configuration parsing with file I/O and JSON parsing
- HTTP client with retry logic and error mapping
- Database transactions with rollback on error
- File processing pipelines with multi-step transformations
Always include:
- Domain-specific error types
- Proper error handling at boundaries (e.g., HTTP responses)
- Functional composition when appropriate
Best Practices
When implementing error-as-value pattern:
-
Consult guidelines first: Reference
for:references/guidelines.md- When to use Result vs exceptions
- Error type design patterns
- Common patterns and anti-patterns
- Migration strategies
- Performance considerations
-
Design error types carefully:
- Use enums for discrete error cases
- Use sealed interfaces when errors need data
- Make errors domain-specific and meaningful
-
Avoid mixing patterns:
- Don't throw exceptions from Result-returning methods
- Don't silently ignore errors with unwrap()
- Be consistent within a module or layer
-
Preserve error information:
- Don't convert errors to generic strings too early
- Map errors at boundaries (e.g., DB error → HTTP status)
- Include context in error types when needed
-
Use appropriate combinators:
for operations returning ResultandThen()
for pure transformationsmap()
for exhaustive handlingmatch()
/ifOk()
for side effects onlyifErr()
-
Test both paths:
- Test success cases with
andisOk()unwrap() - Test error cases with
andisErr()unwrapErr() - Test chaining behavior and short-circuiting
- Test success cases with
Workflow
When a user requests error-as-value implementation:
-
Understand the context:
- Is this new code or refactoring?
- What are the error conditions?
- How should errors be handled?
-
Copy Result type (if not already in project):
- Copy
to appropriate packageassets/Result.java - Adjust package name if needed
- Copy
-
Design error types:
- Define enum or sealed interface for domain errors
- Consult
for design patternsreferences/guidelines.md
-
Implement the logic:
- Use
andResult.ok()
for explicit returnsResult.err() - Use
to wrap exception-throwing codeResult.of() - Use
to chain fallible operationsandThen() - Use
to transform success valuesmap()
- Use
-
Handle at boundaries:
- Use
ormatch()
/ifOk()
at API boundariesifErr() - Convert errors to appropriate formats (HTTP status, log messages, etc.)
- Use
-
Provide examples when helpful:
- Show before/after for refactoring
- Demonstrate chaining for complex flows
- Reference
for realistic patternsreferences/examples.md
-
Write tests:
- Test both success and error paths
- Verify chaining and short-circuit behavior
Resources
assets/Result.java
The complete Result<T, E> implementation ready to copy into any project. This file includes:
- Sealed interface with Ok and Err records (requires Java 17+)
- Complete API: constructors, type checks, extraction, transformation, chaining
- Exception wrapping utility (
)Result.of() - Comprehensive JavaDoc documentation
Usage: Copy this file to the project's source directory and adjust the package name as needed.
references/guidelines.md
Comprehensive guidelines for using the error-as-value pattern effectively:
- When to use Result vs exceptions
- Error type design patterns (String, enum, sealed interface)
- Common usage patterns with code examples
- Naming conventions for methods and error types
- Migration strategy from exception-based code
- Performance considerations
- Testing approaches
- Common pitfalls and how to avoid them
Usage: Reference this document when designing error types, making architectural decisions, or helping users understand best practices.
references/examples.md
Real-world examples demonstrating the pattern in various scenarios:
- User registration service with validation and database operations
- Configuration parser with file I/O and JSON parsing
- HTTP client with retry logic and error mapping
- Database transaction handling with rollback
- Multi-step validation pipeline with error accumulation
- File processing pipeline with transformations
Usage: Use these examples as templates for common scenarios. Show relevant examples to users when implementing similar functionality.
Note: This skill requires Java 17+ for sealed interfaces and records. For older Java versions, the Result type can be adapted to use regular classes instead of records.