Functions

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 created
  • callsite - Where in the code it was created
  • trace - 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);

See Also

  • Result - Simple success/failure type
  • Maybe - Optional values
  • Try - Wrapping code that might throw

On this page