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¶
Python 3.13 or later
Katharos installed (see Getting Started with Katharos)
Complete the Handling Null Values with Maybe tutorial so you are familiar with
Maybe
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.