Attempts

Fluent functional container for handling nullable values with validation pipelines

Attempts in BoxLang are an enhanced Java Optional that provides a fluent, functional approach to handling nullable values. Think of an Attempt as a smart container that can hold a value or be empty, with powerful methods for safely working with that value without null pointer exceptions.

🌟 Why Use Attempts?

Attempts provide several advantages over traditional null handling:

  • Eliminate null checks - No more if (isNull(value)) everywhere

  • Fluent API - Chain operations naturally: .map().filter().orElse()

  • Built-in validation - Attach validation rules to create pipelines

  • Immutable - Chain methods without mutating original values

  • Functional programming - Write declarative, composable code

  • Error handling - Convert missing values to exceptions or defaults gracefully

Traditional vs Attempt Approach

// ❌ Traditional approach - verbose and error-prone
user = userService.get( rc.id );
if ( isNull( user ) ) {
    throw( "UserNotFoundException" );
}
email = user.getEmail();
if ( isNull( email ) ) {
    throw( "Email not found" );
}
domain = email.listLast( "@" );

// ✅ Attempt approach - clean and declarative
attempt( userService.get( rc.id ) )
    .flatMap( user -> user.getEmail() )
    .map( email -> email.listLast( "@" ) )
    .ifPresentOrElse(
        domain -> println( "Domain: #domain#" ),
        () -> throw( "User or email not found" )
    );

Attempts are also unmodifiable, so you can chain methods to handle the value more functionally, but it never mutates the original value. It can also be seeded with validation information to create validation pipelines.

💻 Attempts in Code

Let's see practical examples of Attempts in action:

BoxLang Integration: Many BoxLang BIFs and internal operations return Attempts, making it easy to integrate attempt-based patterns throughout your codebase.

🔄 Attempt States

An Attempt can exist in only two states:

State Checking Methods

Method
Returns
Description

isEmpty()

boolean

✅ Core check - true if no value present

isPresent()

boolean

✅ Core check - true if value exists

isNull()

boolean

🔍 Value check - true if value is null

hasFailed()

boolean

📝 Alias for isEmpty() - more readable for error handling

wasSuccessful()

boolean

📝 Alias for isPresent() - more readable for success cases

Rules for Present State

An attempt is present if and only if:

  • ✅ The value is not null

An attempt is empty if:

  • ❌ The value is null

  • ❌ Created with attempt() (no arguments)

Examples

🏗️ Creating Attempts

Create attempts using the attempt() BIF (Built-In Function).

Empty Attempt

Create an empty attempt by calling attempt() with no arguments:

Immutability: Remember that attempts are immutable - you cannot change an empty attempt to a present one, or vice versa. Each operation returns a new attempt.

Attempt with Value

Pass any value or expression to create an attempt that may be present or empty:

Creation Patterns

🛠️ Working with Attempts

Once you have an attempt, you can interact with it using a rich set of fluent methods. These methods fall into several categories:

📖 Method Reference

🔍 State Checking Methods

isEmpty():boolean

Returns true if the attempt has no value (is null).

isPresent():boolean

Returns true if the attempt contains a value (not null).

isNull():boolean

Returns true if the value is null.

hasFailed():boolean / wasSuccessful():boolean

Fluent aliases for isEmpty() and isPresent() for more readable code.

equals( object ):boolean

Compare two attempts for equality.

🎯 Value Retrieval Methods

get():any / getOrFail():any

Get the value of the attempt. Throws exception if the attempt is empty.

orElse( defaultValue ):any / getOrDefault( defaultValue ):any

Returns the value if present, otherwise returns the provided default.

orElseGet( supplier ):any / getOrSupply( supplier ):any

Returns the value if present, otherwise executes the supplier function and returns its result.

orThrow( [type], [message|exception] ):any

Returns the value if present, otherwise throws an exception.

🔄 Transformation Methods

map( mapper ):attempt

Transform the value if present by applying a function. Returns a new attempt with the transformed value.

Map vs FlatMap: Use map() when your mapper function returns a regular value. Use flatMap() when your mapper function returns an Attempt.

filter( predicate ):attempt

If a value is present and matches the given predicate, returns an Attempt with the value; otherwise, returns an empty Attempt.

flatMap( mapper ):attempt

Apply a function that returns an Attempt, flattening the result to avoid nested Attempts. Use this when your mapping function itself returns an Attempt.

The Problem: If getEmail() returns an Attempt and you use map(), you get Attempt<Attempt<String>> (nested).

The Solution: flatMap() automatically unwraps one level, giving you Attempt<String>.

Complete Example: Before and After

Chaining Multiple FlatMaps

🎭 Conditional Action Methods

ifPresent( action ):attempt / ifSuccessful( action ):attempt

Execute an action if a value is present. Returns the same attempt for chaining.

ifEmpty( action ):attempt / ifFailed( action ):attempt

Execute an action if the attempt is empty. Returns the same attempt for chaining.

ifPresentOrElse( presentAction, emptyAction ):attempt

Execute one of two actions based on whether the value is present.

🔗 Chaining and Fallback Methods

or( supplier ):attempt

If empty, returns a new Attempt produced by the supplier function. Great for fallback chains.

🌊 Stream Integration

stream():Stream<T>

Convert the attempt to a Java Stream. If present, returns a single-element stream; if empty, returns an empty stream.

toOptional():Optional<T>

Convert the attempt to a Java Optional for interoperability with Java code.

🔧 Utility Methods

toString():String

Get a string representation of the attempt.

hashCode():int / equals( object ):boolean

Standard Java object methods for use in collections and comparisons.

✅ Validation Pipelines

Attempts provide powerful validation capabilities that let you attach validation rules and create validation pipelines. This is perfect for input validation, business rules, and data quality checks.

Validation Flow

Validation Process

  1. Attach Matcher - Use to{Method}() to register validation rules

  2. Check Validity - Use isValid():boolean to test if value matches rules

  3. Conditional Actions - Use ifValid() / ifInvalid() for conditional execution

🎯 Validation Matchers

toBe( value ):attempt

Validate that the value exactly equals another value.

toBeBetween( min, max ):attempt

Validate that a numeric value falls within a range (inclusive).

toBeType( type ):attempt

Validate against BoxLang types using the isValid() BIF type system.

Type Reference: See the isValid() BIF documentation for all available type validators.

toMatchRegex( pattern, [caseSensitive=true] ):attempt

Validate against a regular expression pattern.

toSatisfy( predicate ):attempt

Custom validation using a closure/lambda. Most flexible approach.

🎨 Validation Method Reference

Method
Returns
Description

isValid()

boolean

✅ Returns true if value is present AND passes validation rules

ifValid( action )

attempt

🎯 Executes action if value is valid

ifInvalid( action )

attempt

❌ Executes action if value is invalid or empty

🔗 Validation Pipeline Examples

Multi-Stage Validation

API Input Validation

Form Validation Pipeline

💡 Validation Best Practices

  1. Attach validators early - Register validation rules as soon as you create the attempt

  2. Use specific validators - Prefer toBeType() and toMatchRegex() over generic toSatisfy() when possible

  3. Chain validations - Combine multiple validators for complex rules

  4. Handle both paths - Use both ifValid() and ifInvalid() for complete flow control

  5. Return attempts from validators - Enable fluent validation chains

  6. Document validation rules - Comment complex toSatisfy() predicates

  7. Test empty cases - Remember that empty attempts are always invalid


📚 Best Practices Summary

When to Use Attempts

Good Use Cases:

  • External API calls - Network requests that may fail or return null

  • Database queries - Queries that may return no results

  • Configuration lookups - Config values that may not exist

  • File operations - File reads that may fail

  • User input processing - Form data that may be missing or invalid

  • Optional calculations - Operations that may not produce a result

  • Service responses - Backend services that may timeout or error

Avoid Using Attempts For:

  • Simple null checks - Use elvis operator instead

  • Values that are never null - Unnecessary overhead

  • Control flow logic - Use conditionals instead

  • Exception handling - Use try/catch for exception control

Design Patterns

1. 🎯 Fail Fast with orThrow()

2. 🔄 Transform Chains

3. 🎭 Conditional Logic

4. 🔗 Fallback Chains

5. ✅ Validation Pipelines

Performance Considerations

  1. Immutability Cost - Each operation creates a new attempt; minimize in hot loops

  2. Validation Overhead - Matchers add small overhead; cache validation results if needed

  3. Closure Creation - Lambdas in map/flatMap have allocation cost; extract to functions in tight loops

  4. Stream Integration - Converting to streams adds overhead; only use when needed

Code Style Guidelines

  1. Use descriptive variable names for attempts:

  2. Prefer method chaining for readability:

  3. Extract complex lambdas to separate functions:

  4. Use orElse() for simple defaults, orElseGet() for computed defaults:

Common Mistakes to Avoid

Don't call get() without checking:

Don't use map() when you need flatMap():

Don't ignore validation rules:

Don't create attempts for values you know exist:



🎓 Summary

The Attempt class is BoxLang's answer to safe optional value handling with built-in validation. It provides:

  • Null Safety - Eliminates null pointer errors

  • Validation - Built-in type and custom validation rules

  • Fluent API - Chainable operations for clean code

  • Functional Style - Map, filter, flatMap for transformations

  • Explicit Handling - Forces you to think about empty cases

  • Composability - Easily combine with other attempts

Use attempts whenever you're dealing with values that might not exist or might be invalid, and let the type system guide you to handle both cases correctly.

Last updated

Was this helpful?