Outcome
Advanced result system with causes and exceptions
The Outcome type is an advanced result system that distinguishes between three types of results: success, business failures (causes), and system errors (exceptions).
Why Use Outcome?
Unlike simple Result types that only have success/failure, Outcome distinguishes between:
- Success - Operation succeeded with a value
- Failure (Causes) - Expected business failures (validation errors, not found, unauthorized)
- Exception (Errors) - Unexpected system errors (database connection, network timeout)
This distinction is crucial because:
- Business failures are expected and should be handled gracefully
- System exceptions are unexpected and may need special handling (logging, monitoring)
- Different handling strategies for each type of failure
Three States
Success
Operation completed successfully:
import { Outcome } from '@deessejs/functions';
const result = Outcome.success({ id: 123, name: "Alice" });
// { _tag: "Success", value: { id: 123, name: "Alice" } }Failure (Causes)
Expected business failures:
import { Outcome, Cause } from '@deessejs/functions';
const notFound = Outcome.failure(
Cause({
name: "UserNotFound",
message: "User with ID '123' was not found",
data: { userId: "123" },
})
);
const validationError = Outcome.failure(
Cause({
name: "ValidationError",
message: "Email is required",
data: { field: "email" },
})
);Exception (Errors)
Unexpected system errors:
import { Outcome, Exception } from '@deessejs/functions';
const dbError = Outcome.exception(
Exception({
name: "DatabaseError",
message: "Failed to connect to database",
data: { host: "localhost", port: 5432 },
})
);
// Convert Error objects to exceptions
const error = new Error("Connection timeout");
const ex = Outcome.exception(Exception.fromError(error));Pattern Matching
Use match() to handle all three cases:
import { Outcome } from '@deessejs/functions';
const message = result.match({
onSuccess: (value) => `User: ${value.name}`,
onFailure: (causes) => {
const cause = causes[0];
return `Business error: ${cause.name} - ${cause.message}`;
},
onException: (errors) => {
const error = errors[0];
return `System error: ${error.name} - ${error.message}`;
},
});Type Guards
Check the outcome type with type guards:
import { Outcome } from '@deessejs/functions';
function handleOutcome(outcome: Outcome<User, Cause, Exception>) {
if (outcome.isSuccess()) {
// TypeScript knows: outcome.value exists
console.log("Success:", outcome.value);
} else if (outcome.isFailure()) {
// TypeScript knows: outcome.causes exists
console.error("Failure:", outcome.causes);
} else if (outcome.isException()) {
// TypeScript knows: outcome.errors exists
console.error("Exception:", outcome.errors);
}
}Common Causes
Use built-in helpers for common business failures:
import { Causes } from '@deessejs/functions';
// Resource not found
Causes.notFound("123", "User");
// { name: "NotFound", message: "User with ID '123' was not found" }
// Validation error
Causes.validation("Email is invalid", { field: "email" });
// { name: "ValidationError", message: "Email is invalid", data: { field: "email" } }
// Unauthorized
Causes.unauthorized("Invalid credentials");
// { name: "Unauthorized", message: "Invalid credentials" }
// Forbidden
Causes.forbidden("Insufficient permissions");
// { name: "Forbidden", message: "Insufficient permissions" }
// Conflict
Causes.conflict("Email already exists", { email: "test@example.com" });
// { name: "Conflict", message: "Email already exists" }Common Exceptions
Use built-in helpers for common system errors:
import { Exceptions } from '@deessejs/functions';
// Internal server error
Exceptions.internal("Unexpected error occurred");
// Database error
Exceptions.database("Failed to connect to database");
// Network error
Exceptions.network("Connection refused");
// Timeout
Exceptions.timeout("fetchUsers", 5000);
// { name: "TimeoutError", message: "Operation 'fetchUsers' timed out after 5000ms" }
// Not implemented
Exceptions.notImplemented("deleteUser");
// { name: "NotImplementedError", message: "Feature 'deleteUser' is not implemented" }Creating Custom Causes
Define domain-specific causes:
import { Cause } from '@deessejs/functions';
import { z } from 'zod';
// Simple cause
const userNotFound = (userId: string) =>
Cause({
name: "UserNotFound",
message: `User '${userId}' not found`,
data: { userId },
});
// With Zod schema for type safety
const invalidEmail = Cause.withSchema({
name: "InvalidEmail",
message: "Email address is invalid",
schema: z.object({
email: z.string().email(),
reason: z.enum(["format", "domain", "disposable"]),
}),
});
// Usage
const outcome = Outcome.failure(
invalidEmail({ email: "test@invalid", reason: "domain" })
);Metadata and Tracing
Outcomes include metadata for debugging:
import { Outcome, withTrace, pipe } from '@deessejs/functions';
const outcome = Outcome.success({ id: 123 });
// Add trace information
const traced = withTrace(outcome, "User fetched successfully");
// Pipe through functions with automatic tracing
const result = pipe(
outcome,
(user) => Outcome.success(user.email),
"Extracted email"
);Each outcome includes:
timestamp- When the outcome was createdcallsite- Where in the code it was createdtrace- Array of steps for debugging
Combining Causes and Exceptions
Combine multiple failures:
import { Outcome, Cause, Exception } from '@deessejs/functions';
// Multiple validation errors
const errors = Cause.combine([
Causes.validation("Email is required"),
Causes.validation("Password is too short"),
]);
// Multiple system errors
const exceptions = Exception.combine([
Exceptions.database("Connection failed"),
Exceptions.network("API unreachable"),
]);Real-World Example
import { Outcome, Cause, Exception, Causes, Exceptions } from '@deessejs/functions';
async function login(email: string, password: string): Promise<Outcome<Session, Cause, Exception>> {
// Validate input
if (!email || !password) {
return Outcome.failure(
Causes.validation("Email and password are required")
);
}
try {
// Find user
const user = await db.users.findByEmail(email);
if (!user) {
return Outcome.failure(
Causes.notFound(email, "User")
);
}
// Check password
if (!await verifyPassword(password, user.passwordHash)) {
return Outcome.failure(
Causes.unauthorized("Invalid password")
);
}
// Create session
const session = await createSession(user);
return Outcome.success(session);
} catch (error) {
// Handle unexpected system errors
return Outcome.exception(
Exceptions.database("Failed to authenticate user")
);
}
}
// Usage
const outcome = await login("user@example.com", "password");
outcome.match({
onSuccess: (session) => {
console.log("Logged in:", session.token);
},
onFailure: (causes) => {
// Handle expected business failures
const cause = causes[0];
if (cause.name === "Unauthorized") {
console.log("Invalid credentials");
} else if (cause.name === "ValidationError") {
console.log("Please fill all fields");
} else if (cause.name === "NotFound") {
console.log("User not found");
}
},
onException: (errors) => {
// Handle unexpected system errors
console.error("System error - please try again later");
logError(errors[0]);
},
});Best Practices
1. Use Causes for Expected Failures
// ✅ Good - business failure
return Outcome.failure(Causes.notFound(userId));
// ❌ Bad - using exception for business logic
return Outcome.exception(Exceptions.internal("User not found"));2. Use Exceptions for System Errors
// ✅ Good - system error
try {
await db.connect();
} catch (error) {
return Outcome.exception(Exceptions.database(error.message));
}
// ❌ Bad - using cause for system error
return Outcome.failure(Cause({ name: "DatabaseError", ... }));3. Provide Helpful Data
// ✅ Good - includes helpful context
Causes.validation("Email is required", {
field: "email",
form: "login",
});
// ❌ Bad - no context
Cause({ name: "ValidationError", message: "Invalid data" });4. Handle All Three Cases
Always handle success, failure, and exception:
// ✅ Good - comprehensive handling
outcome.match({
onSuccess: (value) => render(value),
onFailure: (causes) => showError(causes[0].message),
onException: (errors) => reportError(errors[0]),
});
// ❌ Bad - incomplete handling
if (outcome.isSuccess()) {
render(outcome.value);
}
// What about failures and exceptions?Converting From/To Result
Convert between Outcome and legacy Result types:
import { Outcome, Result } from '@deessejs/functions';
// Convert Result to Outcome
const outcome = Outcome.fromResult(
Result.success(42),
"CustomError"
);
// Convert Outcome to Result
const result = Outcome.toResult(outcome);