Functional Programming Increases LLM Signal

James Phoenix
James Phoenix

Errors as values, typed composition, and explicit effects give LLMs complete signal on what code actually does.

Author: James Phoenix | Date: November 2025


The Core Insight

Traditional exception-based code hides information from LLMs. When errors are implicit, the model can’t know what might fail or how to handle it.

Functional programming patterns expose this signal explicitly.


Effect.ts: Haskell-Level FP in TypeScript

Effect brings typed, composable effects to TypeScript:

  • Everything becomes a typed composable dynamic graph
  • LLMs have complete signal on errors AND values
  • Similar to Rust/Go/Haskell where errors are values in the function signature

The Problem: Implicit Exceptions

# Standard Python - errors are IMPLICIT
def do_something(x):
    try:
        if x < 0:
            raise ValueError("x must be >= 0")
        return x * 2
    except Exception as e:
        return e

What the LLM sees: A function that takes x and returns... something. The exception handling is invisible in the signature.

What’s hidden:

Leanpub Book

Read The Meta-Engineer

A practical book on building autonomous AI systems with Claude Code, context engineering, verification loops, and production harnesses.

Continuously updated
Claude Code + agentic systems
View Book
  • This function can fail
  • It fails with ValueError specifically
  • The caller has no typed signal about failure modes

The Solution: Errors as Values

# Errors as values - errors are EXPLICIT
def do_something(x) -> tuple[Exception | None, int | None]:
    if x < 0:
        return ValueError("x must be >= 0"), None
    return None, x * 2

What the LLM sees: A function that returns either an error OR a value. The signature tells the complete story.

What’s explicit:

  • Function can fail (return type includes Exception)
  • Success case returns int
  • Caller MUST handle both cases

Effect.ts Examples

Traditional TypeScript (Hidden Errors)

// Implicit failures - LLM has no signal
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error("Failed to fetch user");
  }
  return response.json();
}

// Caller has no idea this can fail
const user = await fetchUser("123");

Effect TypeScript (Explicit Errors)

import { Effect, Context } from "effect";

// Errors are in the type signature
class FetchError {
  readonly _tag = "FetchError";
  constructor(readonly message: string) {}
}

class ParseError {
  readonly _tag = "ParseError";
  constructor(readonly message: string) {}
}

// Effect<Success, Error, Requirements>
const fetchUser = (id: string): Effect.Effect<User, FetchError | ParseError> =>
  Effect.tryPromise({
    try: () => fetch(`/api/users/${id}`),
    catch: () => new FetchError("Network request failed"),
  }).pipe(
    Effect.flatMap((response) =>
      response.ok
        ? Effect.tryPromise({
            try: () => response.json() as Promise<User>,
            catch: () => new ParseError("Invalid JSON"),
          })
        : Effect.fail(new FetchError(`HTTP ${response.status}`))
    )
  );

// LLM knows EXACTLY what can fail
// Effect<User, FetchError | ParseError, never>

Why This Matters for LLMs

Aspect Implicit Exceptions Errors as Values
Type signature Lies about what happens Complete truth
Failure modes Hidden in implementation Visible in types
Composition Try/catch spaghetti Typed pipelines
LLM comprehension Must read all code Signature is enough

The Composability Advantage

Effect makes everything a typed, composable graph:

// Each step's errors are tracked and composed
const program = pipe(
  fetchUser(userId),           // Effect<User, FetchError | ParseError>
  Effect.flatMap(validateUser), // Effect<ValidUser, FetchError | ParseError | ValidationError>
  Effect.flatMap(saveToDb),     // Effect<void, FetchError | ParseError | ValidationError | DbError>
  Effect.catchTag("DbError", handleDbError), // Handle specific error
);

// Final type tells the COMPLETE story:
// Effect<void, FetchError | ParseError | ValidationError>

An LLM reading this code knows:

  1. What operations happen
  2. What can fail at each step
  3. Which errors are handled vs propagated
  4. The exact requirements and outputs

Go/Rust Pattern in TypeScript

The same principle, simpler syntax:

// Result type (like Rust's Result<T, E>)
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

function divide(a: number, b: number): Result<number, "DivisionByZero"> {
  if (b === 0) {
    return { ok: false, error: "DivisionByZero" };
  }
  return { ok: true, value: a / b };
}

// Caller MUST handle both cases
const result = divide(10, 0);
if (!result.ok) {
  console.error(result.error); // TypeScript knows this is "DivisionByZero"
} else {
  console.log(result.value); // TypeScript knows this is number
}

Key Takeaway

When errors are values in the type signature, LLMs have complete signal. They don’t need to trace through implementations to understand what can fail.

Increased signal → Better LLM code generation → Fewer bugs


Related


Resources

Explore it visually

Topics
Functional ProgrammingInformation TheoryReliabilityType Safety

Newsletter

Become a better AI engineer

Weekly deep dives on production AI systems, context engineering, and the patterns that compound. No fluff, no tutorials. Just what works.

Join 306K+ developers. No spam. Unsubscribe anytime.


More Insights

Cover Image for Why Unrealistic /goal Targets Keep Coding Agents Working Longer

Why Unrealistic /goal Targets Keep Coding Agents Working Longer

I used an unrealistic tutor-discovery target to keep Codex working beyond a finished-sounding handover. Here is the run, the results and the reusable prompt.

James Phoenix
James Phoenix
Cover Image for Why I Threw Away My Video Harness When Opus 5.5 Arrived

Why I Threw Away My Video Harness When Opus 5.5 Arrived

I spent £700 on video analysis. A week later, Opus 5.5 outperformed my custom harness and changed how I think about constraints, consistency and model progress.

James Phoenix
James Phoenix