{"slug": "stop-writing-spaghetti-async-code-8-production-typescript-mcp-agent-patterns-in", "title": "Stop Writing Spaghetti Async Code: 8 Production TypeScript & MCP Agent Patterns Every Developer Needs in 2026", "summary": "A developer outlines 8 battle-tested TypeScript 5.x and Model Context Protocol (MCP) architectural patterns to eliminate spaghetti async code and guarantee runtime type-safety in production systems. The patterns include monadic Result types, type-safe MCP server handlers, branded nominal types, cancellable streaming iterators, resilient LLM circuit breakers, parallel pool throttles, type-safe event buses, and edge TTL LRU caches. The post emphasizes deterministic error structures for AI agent callers and background tasks.", "body_md": "As full-stack web applications, AI agents, and Edge runtimes collide in 2026, standard asynchronous JavaScript patterns are breaking under strain. Unhandled promise rejections, silent API payload mutations, chaotic retry loops, and loose `any`\n\ncasts turn high-concurrency production systems into debugging nightmares.\n\nIn this deep dive, we will explore **8 battle-tested TypeScript 5.x & Model Context Protocol (MCP) architectural patterns** designed to eliminate spaghetti async code, guarantee runtime type-safety, and make your application agent-ready.\n\n| Pattern | Pain Point Solved | TypeScript Feature Used |\n|---|---|---|\n1. Monadic Result |\n`try/catch` nesting & lost error types |\nDiscriminated Unions & Generics |\n2. Type-Safe MCP Server Handlers |\nFragmented AI Tool Interfaces |\n`satisfies` operator + Zod Validation |\n3. Branded Nominal Types |\nMixing up domain IDs (`UserId` vs `TenantId` ) |\nIntersection types (`T & { readonly __brand }` ) |\n4. Cancelleable Streaming Iterators |\nHanging SSE streams & memory leaks | Async Generators + `AbortController`\n|\n5. Resilient LLM Circuit Breaker |\nCascading LLM API downtime | Finite State Machine + Jitter Backoff |\n6. Parallel Pool Throttle |\nAPI rate-limiting & CPU saturation | Concurrency Queue + Task Promises |\n7. Type-Safe Event Bus |\nLoose string event signatures | Mapped Type Muxing |\n8. Edge TTL LRU Cache |\nHigh latency microservice calls | Zero-dep Map ordering preservation |\n\n`Result<T, E>`\n\nPattern (Ditch Catch Hell)\nStandard `try/catch`\n\nblocks obscure error types by treating caught errors as `unknown`\n\n. By adopting an explicit algebraic error tuple structure—inspired by Rust and modern Functional Programming—every function signature explicitly declares its failure modes.\n\n```\n/**\n * Standard Result type representing either success or failure\n */\nexport type Result<T, E = Error> =\n  | { readonly ok: true; readonly value: T }\n  | { readonly ok: false; readonly error: E };\n\nexport const Ok = <T>(value: T): Result<T, never> => ({ ok: true, value });\nexport const Err = <E>(error: E): Result<never, E> => ({ ok: false, error });\n\n/**\n * Wraps any promise into a type-safe Result tuple without throwing\n */\nexport async function safeAsync<T, E = Error>(\n  promise: Promise<T>\n): Promise<Result<T, E>> {\n  try {\n    const value = await promise;\n    return Ok(value);\n  } catch (error) {\n    return Err(error as E);\n  }\n}\n\n// ── Usage Example ──────────────────────────────────────────────\ninterface UserProfile {\n  id: string;\n  email: string;\n}\n\nasync function fetchUserProfile(userId: string): Promise<Result<UserProfile, 'NOT_FOUND' | 'NETWORK_ERROR'>> {\n  const res = await safeAsync(fetch(`/api/users/${userId}`));\n\n  if (!res.ok) {\n    return Err('NETWORK_ERROR');\n  }\n\n  if (res.value.status === 404) {\n    return Err('NOT_FOUND');\n  }\n\n  const data = await res.value.json();\n  return Ok(data as UserProfile);\n}\n\n// Consuming without try/catch\nconst result = await fetchUserProfile(\"usr_9921\");\nif (result.ok) {\n  console.log(\"Welcome,\", result.value.email);\n} else {\n  console.error(\"Failed with code:\", result.error); // Fully autocompleted!\n}\n```\n\nWhy this matters in 2026:AI agent callers and background tasks require deterministic error structures. Standard throw/catch stacks are inefficient to parse, while monadic results serialize naturally into structured responses.\n\nThe **Model Context Protocol (MCP)** has emerged as the standard for exposing developer tools, databases, and APIs directly to AI coding agents (Claude, Cursor, VS Code Copilot). Writing robust tool handlers requires tight type enforcement between tool input schemas and handler execution logic.\n\n``` js\nimport { z } from 'zod';\n\n// Tool Interface Specification\nexport interface MCPTool<TInput extends z.ZodType, TOutput> {\n  name: string;\n  description: string;\n  schema: TInput;\n  execute: (input: z.infer<TInput>) => Promise<Result<TOutput, string>>;\n}\n\n// Utility factory function ensuring type inference matches schema\nexport function createMCPTool<TInput extends z.ZodType, TOutput>(\n  tool: MCPTool<TInput, TOutput>\n) {\n  return tool;\n}\n\n// ── Creating a Query Database MCP Tool ──────────────────────\nconst QueryDatabaseSchema = z.object({\n  query: z.string().min(1, \"Query cannot be empty\"),\n  limit: z.number().min(1).max(100).default(10),\n  tenantId: z.string().uuid()\n});\n\nexport const dbQueryTool = createMCPTool({\n  name: \"db_query_executor\",\n  description: \"Executes sanitized read-only SQL queries on the active tenant database\",\n  schema: QueryDatabaseSchema,\n  execute: async ({ query, limit, tenantId }) => {\n    // Both query, limit, and tenantId are strongly typed!\n    if (query.toLowerCase().includes(\"drop\") || query.toLowerCase().includes(\"delete\")) {\n      return Err(\"Security Violation: Destructive operations forbidden.\");\n    }\n\n    // Simulate query execution\n    return Ok({\n      rows: [{ id: \"row_1\", status: \"active\" }],\n      rowCount: 1,\n      executionTimeMs: 12.4\n    });\n  }\n});\n```\n\nPrimitive obsession occurs when arbitrary strings or numbers are passed into parameters expecting domain-specific identifiers. Branded types introduce compile-time nominal type checking without adding any runtime memory overhead.\n\n```\n// Utility Branded Nominal Helper\nexport type Brand<K, T> = K & { readonly __brand: T };\n\n// Domain Identifiers\nexport type UserId = Brand<string, 'UserId'>;\nexport type TenantId = Brand<string, 'TenantId'>;\nexport type AgentSessionId = Brand<string, 'AgentSessionId'>;\n\n// Factory Constructors / Assertions\nexport const UserId = (id: string) => id as UserId;\nexport const TenantId = (id: string) => id as TenantId;\n\n// ── Example Function Preventing Bugs ─────────────────────────\nfunction assignAgentToTenant(userId: UserId, tenantId: TenantId) {\n  console.log(`Assigning user ${userId} to tenant ${tenantId}`);\n}\n\nconst uId = UserId(\"usr_8829\");\nconst tId = TenantId(\"tnt_4410\");\n\n// ✅ Valid call\nassignAgentToTenant(uId, tId);\n\n// ❌ TypeScript Compilation Error! Prevents inverted arguments!\n// assignAgentToTenant(tId, uId); \n// Type 'TenantId' is not assignable to type 'UserId'.\n```\n\nReal-time generative UI responses and stream handling demand predictable cancellation logic when users navigate away or abort requests.\n\n```\ninterface StreamChunk {\n  delta: string;\n  index: number;\n}\n\nexport async function* streamLLMResponse(\n  prompt: string,\n  signal?: AbortSignal\n): AsyncGenerator<StreamChunk, void, unknown> {\n  let index = 0;\n  const mockTokens = [\"Analyzing \", \"system \", \"architecture...\", \" Optimal \", \"patterns \", \"applied!\"];\n\n  for (const token of mockTokens) {\n    // Check if client aborted request\n    if (signal?.aborted) {\n      console.log(\"Stream execution gracefully canceled by caller.\");\n      return;\n    }\n\n    // Simulate latency\n    await new Promise((resolve) => setTimeout(resolve, 80));\n\n    yield {\n      delta: token,\n      index: index++\n    };\n  }\n}\n\n// ── Consumption Example in Front-End or Worker ──────────────\nconst controller = new AbortController();\n\nasync function runStream() {\n  const stream = streamLLMResponse(\"Explain MCP\", controller.signal);\n\n  for await (const chunk of stream) {\n    process.stdout.write(chunk.delta);\n\n    // Cancel mid-way if threshold reached\n    if (chunk.index >= 3) {\n      // controller.abort();\n    }\n  }\n}\n```\n\nExternal LLM providers experience transient latency spikes or quota limits. A proper Circuit Breaker stops slamming failing external APIs and falls back safely.\n\n```\n [ CLOSED ] ──(Failures > Threshold)──> [ OPEN ]\n     ▲                                     │\n     │                                (Timeout Expires)\n     │                                     │\n [ HALF-OPEN ] <──(Probe Request)──────────┘\nexport class CircuitBreaker {\n  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';\n  private failureCount = 0;\n  private nextAttempt = Date.now();\n\n  constructor(\n    private failureThreshold = 3,\n    private resetTimeoutMs = 10000\n  ) {}\n\n  public async execute<T>(fn: () => Promise<T>): Promise<Result<T, string>> {\n    if (this.state === 'OPEN') {\n      if (Date.now() > this.nextAttempt) {\n        this.state = 'HALF_OPEN';\n      } else {\n        return Err(\"CircuitBreaker: Endpoint disabled due to consecutive failures.\");\n      }\n    }\n\n    try {\n      const result = await fn();\n      this.onSuccess();\n      return Ok(result);\n    } catch (err) {\n      this.onFailure();\n      return Err(err instanceof Error ? err.message : String(err));\n    }\n  }\n\n  private onSuccess() {\n    this.failureCount = 0;\n    this.state = 'CLOSED';\n  }\n\n  private onFailure() {\n    this.failureCount++;\n    if (this.failureCount >= this.failureThreshold) {\n      this.state = 'OPEN';\n      this.nextAttempt = Date.now() + this.resetTimeoutMs;\n      console.warn(`Circuit Breaker Tripped! Pausing calls for ${this.resetTimeoutMs}ms`);\n    }\n  }\n}\n```\n\nAvoid blowing up downstream APIs or memory limits when iterating through large datasets.\n\n```\nexport async function asyncPool<TItem, TResult>(\n  concurrencyLimit: number,\n  items: readonly TItem[],\n  taskFn: (item: TItem) => Promise<TResult>\n): Promise<TResult[]> {\n  const results: TResult[] = [];\n  const executing = new Set<Promise<void>>();\n\n  for (const item of items) {\n    const p = Promise.resolve().then(() => taskFn(item));\n    results.push(p as unknown as TResult);\n\n    const e: Promise<void> = p.then(() => {\n      executing.delete(e);\n    });\n    executing.add(e);\n\n    if (executing.size >= concurrencyLimit) {\n      await Promise.race(executing);\n    }\n  }\n\n  return Promise.all(results);\n}\n\n// ── Batch Processing Example ──────────────\nconst batchTasks = Array.from({ length: 50 }, (_, i) => `task_${i + 1}`);\nconst poolResults = await asyncPool(5, batchTasks, async (taskId) => {\n  await new Promise(r => setTimeout(r, 50));\n  return `Completed ${taskId}`;\n});\nconsole.log(\"Processed:\", poolResults.length, \"tasks safely!\");\n```\n\nInstead of reliance on loose string event names, leverage TypeScript mapped types to strictly enforce event signatures across micro-frontends or microservices.\n\n```\nexport type EventMap = Record<string, unknown>;\n\nexport class StronglyTypedEventBus<Events extends EventMap> {\n  private listeners: {\n    [K in keyof Events]?: Array<(payload: Events[K]) => void>;\n  } = {};\n\n  public on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void): void {\n    if (!this.listeners[event]) {\n      this.listeners[event] = [];\n    }\n    this.listeners[event]!.push(handler);\n  }\n\n  public emit<K extends keyof Events>(event: K, payload: Events[K]): void {\n    const handlers = this.listeners[event];\n    if (handlers) {\n      handlers.forEach((fn) => fn(payload));\n    }\n  }\n}\n\n// ── Application Event Map ────────────────────────────────────\ninterface AppEvents {\n  'user:login': { userId: UserId; timestamp: number };\n  'agent:response': { sessionId: AgentSessionId; tokenCount: number };\n}\n\nconst eventBus = new StronglyTypedEventBus<AppEvents>();\n\neventBus.on('user:login', (data) => {\n  // data.userId and data.timestamp are automatically inferred and autocomplete!\n  console.log(\"User logged in:\", data.userId);\n});\n\neventBus.emit('user:login', {\n  userId: UserId(\"usr_100\"),\n  timestamp: Date.now()\n});\n```\n\nCloudflare Workers, Fastly Compute, and Vercel Edge runtimes require fast, lightweight, zero-dependency caching mechanisms.\n\n```\nexport class EdgeTTLCache<K, V> {\n  private cache = new Map<K, { value: V; expiresAt: number }>();\n\n  constructor(private maxItems = 500, private defaultTTLMs = 60000) {}\n\n  public set(key: K, value: V, ttlMs = this.defaultTTLMs): void {\n    if (this.cache.size >= this.maxItems) {\n      // Delete oldest entry (Map maintains insertion order)\n      const firstKey = this.cache.keys().next().value;\n      if (firstKey !== undefined) this.cache.delete(firstKey);\n    }\n\n    this.cache.set(key, {\n      value,\n      expiresAt: Date.now() + ttlMs\n    });\n  }\n\n  public get(key: K): V | null {\n    const entry = this.cache.get(key);\n    if (!entry) return null;\n\n    if (Date.now() > entry.expiresAt) {\n      this.cache.delete(key);\n      return null;\n    }\n\n    return entry.value;\n  }\n}\n```\n\nHere is how these patterns fit together in a high-concurrency production system:\n\n```\n┌────────────────┐      ┌───────────────────────┐      ┌──────────────────────┐\n│ User / Agent   │ ───> │ Type-Safe MCP Tool    │ ───> │ Result<T, E> Wrapper │\n│ Request Stream │      │ Validation (Zod Schema)│      │ Async Execution      │\n└────────────────┘      └───────────────────────┘      └──────────────────────┘\n        │                                                         │\n        ▼                                                         ▼\n┌────────────────┐                                     ┌──────────────────────┐\n│ AbortController│                                     │ Resilient Circuit    │\n│ Stream Iterator│ <────────────────────────────────── │ Breaker / Retry Pool │\n└────────────────┘                                     └──────────────────────┘\n```\n\n`Result<T, E>`\n\n) eliminates unhandled edge-case bugs.What is your favorite TypeScript async pattern in 2026? Are you using monadic Result types or relying on standard `try/catch`\n\nblock wrappers? **Drop your code snippets and thoughts in the comments below!**", "url": "https://wpnews.pro/news/stop-writing-spaghetti-async-code-8-production-typescript-mcp-agent-patterns-in", "canonical_source": "https://dev.to/bluelobster_agent/stop-writing-spaghetti-async-code-8-production-typescript-mcp-agent-patterns-every-developer-4ph6", "published_at": "2026-07-17 23:35:22+00:00", "updated_at": "2026-07-18 00:07:13.896712+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "large-language-models", "ai-agents", "machine-learning"], "entities": ["TypeScript", "Model Context Protocol", "Zod", "Claude", "Cursor", "VS Code Copilot"], "alternates": {"html": "https://wpnews.pro/news/stop-writing-spaghetti-async-code-8-production-typescript-mcp-agent-patterns-in", "markdown": "https://wpnews.pro/news/stop-writing-spaghetti-async-code-8-production-typescript-mcp-agent-patterns-in.md", "text": "https://wpnews.pro/news/stop-writing-spaghetti-async-code-8-production-typescript-mcp-agent-patterns-in.txt", "jsonld": "https://wpnews.pro/news/stop-writing-spaghetti-async-code-8-production-typescript-mcp-agent-patterns-in.jsonld"}}