Build a User Registration System with Result

In this tutorial, we will build a user registration system that handles errors functionally. Along the way, we will encounter Result, fmap, the | operator for chaining validations, and the @do decorator for combining multiple results cleanly.

Note

Always supply both type arguments when constructing a Result value — use Result[Exception, str].Success("ok") and Result[Exception, str].Failure(err), not Result.Success("ok"). The two type parameters are the error type and the success type; providing them lets your type checker verify each step of the pipeline.

Prerequisites

Step 1: Create Your First Result

Create a new Python file called registration.py with the following contents:

from katharos.types import Result

def check_password_length(password: str) -> Result[Exception, str]:
    if len(password) >= 8:
        return Result[Exception, str].Success(password)
    return Result[Exception, str].Failure(ValueError("Password too short"))

print(check_password_length("secret123"))
print(check_password_length("short"))

Run the file:

python registration.py

You should see:

Success(secret123)
Failure(ValueError('Password too short'))

Notice how the Result wraps a valid password in Success and captures an error in Failure — no exception is raised.

Step 2: Add Email Validation

Now we will add another validation function. Remove the two print lines from the bottom of registration.py and add:

def check_email_format(email: str) -> Result[Exception, str]:
    if "@" in email and "." in email:
        return Result[Exception, str].Success(email)
    return Result[Exception, str].Failure(ValueError("Invalid email format"))

print(check_email_format("user@example.com"))
print(check_email_format("notanemail"))

Run the file:

python registration.py

You should see:

Success(user@example.com)
Failure(ValueError('Invalid email format'))

Step 3: Transform a Success Value with fmap

We will now normalise the email to lowercase using fmap. Replace the two print lines with:

print(check_email_format("User@Example.COM").fmap(lambda email: email.lower()))
print(check_email_format("invalid").fmap(lambda email: email.lower()))

Run the file:

python registration.py

You should see:

Success(user@example.com)
Failure(ValueError('Invalid email format'))

Notice that fmap applied the lowercase transformation only to the Success value. The Failure passed through unchanged without ever calling the lambda.

Step 4: Chain Validations with |

Now we will chain multiple validations using the | operator. Remove the two print lines and add:

def check_password_strength(password: str) -> Result[Exception, str]:
    if any(c.isdigit() for c in password):
        return Result[Exception, str].Success(password)
    return Result[Exception, str].Failure(ValueError("Password must contain a number"))

print(check_password_length("secret123") | check_password_strength)
print(check_password_length("secretword") | check_password_strength)
print(check_password_length("short") | check_password_strength)

Run the file:

python registration.py

You should see:

Success(secret123)
Failure(ValueError('Password must contain a number'))
Failure(ValueError('Password too short'))

Notice how the chain stops at the first Failure. In the third case, check_password_strength is never called because the password was already rejected by check_password_length.

Step 5: Build the Registration Function

Now we will combine everything into a single registration function using the @do decorator. Remove the three print lines and add:

from katharos.syntax_sugar import do, DoBlock

def register_user(email: str, password: str) -> Result[Exception, dict]:
    @do(Result)
    def block() -> DoBlock[dict]:
        validated_email: str = yield check_email_format(email).fmap(lambda e: e.lower())
        validated_password: str = yield check_password_length(password) | check_password_strength
        return {"email": validated_email, "password": validated_password}

    return block()

print(register_user("Alice@Example.com", "secret123"))
print(register_user("notanemail", "secret123"))
print(register_user("alice@example.com", "short"))

Run the file:

python registration.py

You should see:

Success({'email': 'alice@example.com', 'password': 'secret123'})
Failure(ValueError('Invalid email format'))
Failure(ValueError('Password too short'))

Notice that the block stops at the first Failure and returns it immediately, without running the remaining yield steps. Also notice that validated_email and validated_password are real str values inside the block — they can be used directly in the return expression without any placeholder workaround.

Step 6: Extract Values from Results

Now we will extract the user data when registration succeeds. Replace the three print lines at the bottom with:

result = register_user("bob@example.com", "password123")

if result.is_success():
    user_data = result.value
    print(f"User created: {user_data['email']}")
else:
    error = result.error
    print(f"Registration failed: {error}")

result = register_user("invalid", "password123")

if result.is_success():
    user_data = result.value
    print(f"User created: {user_data['email']}")
else:
    error = result.error
    print(f"Registration failed: {error}")

Run the file:

python registration.py

You should see:

User created: bob@example.com
Registration failed: Invalid email format

What We Built

We built a complete user registration system that validates email format, password length, and password strength, chains validations with |, and uses @do to combine multiple results into a single function. Every possible error is captured in the Result type and never thrown as an exception.