Function Patterns
April 1, 2026 · View on GitHub
This document covers every function pattern and idiom used in the Claude Code codebase. These patterns are used consistently and should be followed in all new code.
buildTool() Factory Pattern
All model-invocable tools are created via the buildTool() factory from src/Tool.ts. This provides safe defaults for optional methods:
import { buildTool } from '../../Tool.js'
import type { ToolDef } from '../../Tool.js'
export const GrepTool = buildTool({
name: GREP_TOOL_NAME,
searchHint: 'search file contents with regex (ripgrep)',
maxResultSizeChars: 20_000,
strict: true,
async description() {
return getDescription()
},
userFacingName() {
return 'Search'
},
get inputSchema(): InputSchema {
return inputSchema()
},
get outputSchema(): OutputSchema {
return outputSchema()
},
isConcurrencySafe() {
return true
},
isReadOnly() {
return true
},
async prompt() {
return getDescription()
},
async validateInput({ path }): Promise<ValidationResult> {
// ... validation logic
return { result: true }
},
async checkPermissions(input, context): Promise<PermissionDecision> {
return checkReadPermissionForTool(GrepTool, input, ...)
},
renderToolUseMessage,
renderToolUseErrorMessage,
renderToolResultMessage,
async call(input, context) {
// ... tool execution logic
return { data: output }
},
} satisfies ToolDef<InputSchema, Output>)
Key buildTool() Properties
| Property | Required | Purpose |
|---|---|---|
name | Yes | Tool identifier (PascalCase string) |
inputSchema | Yes | Zod schema (via lazySchema) |
call() | Yes | Main execution function |
description() | Yes | Description for the model |
prompt() | Yes | Full prompt text |
renderToolUseMessage | Yes | Ink rendering of tool use |
renderToolResultMessage | Yes | Ink rendering of result |
searchHint | No | 3-10 word capability phrase for ToolSearch |
maxResultSizeChars | No | Max chars before truncation |
strict | No | Reject unknown input keys |
isReadOnly() | No | Whether tool modifies state |
isConcurrencySafe() | No | Whether tool can run in parallel |
validateInput() | No | Pre-execution validation |
checkPermissions() | No | Permission check |
memoize() Pattern
Use lodash memoize for expensive one-time computations. This is heavily used throughout the codebase:
Basic Memoization
import memoize from 'lodash-es/memoize.js'
export const isDebugMode = memoize((): boolean => {
return (
runtimeDebugEnabled ||
isEnvTruthy(process.env.DEBUG) ||
process.argv.includes('--debug')
)
})
export const getDebugFilter = memoize((): DebugFilter | null => {
const debugArg = process.argv.find(arg => arg.startsWith('--debug='))
if (!debugArg) return null
return parseDebugFilter(debugArg.substring('--debug='.length))
})
export const isDebugToStdErr = memoize((): boolean => {
return process.argv.includes('--debug-to-stderr') || process.argv.includes('-d2e')
})
Clearing Memoize Caches
When underlying state changes, clear the memoize cache:
export function enableDebugLogging(): boolean {
const wasActive = isDebugMode() || process.env.USER_TYPE === 'ant'
runtimeDebugEnabled = true
isDebugMode.cache.clear?.() // Clear so next call re-evaluates
return wasActive
}
export function setSystemPromptInjection(value: string | null): void {
systemPromptInjection = value
getUserContext.cache.clear?.()
getSystemContext.cache.clear?.()
}
Memoize for Async Functions
const updateLatestDebugLogSymlink = memoize(async (): Promise<void> => {
try {
const debugLogPath = getDebugLogPath()
const debugLogsDir = dirname(debugLogPath)
const latestSymlinkPath = join(debugLogsDir, 'latest')
await unlink(latestSymlinkPath).catch(() => {})
await symlink(debugLogPath, latestSymlinkPath)
} catch {
// Silently fail if symlink creation fails
}
})
Arrow Functions vs function Declarations
Arrow Functions: Short Callbacks, Module Constants, Inline Expressions
// Short callbacks
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
results.map(line => toRelativePath(line))
tools.filter(tool => tool.isEnabled())
items.sort((a, b) => b[1] - a[1])
// Module-level memoized constants
export const isDebugMode = memoize((): boolean => { ... })
export const getDebugFilter = memoize((): DebugFilter | null => { ... })
function Declarations: Named Exports, Hoisted Helpers, Complex Logic
// Named exports
export function toError(e: unknown): Error {
return e instanceof Error ? e : new Error(String(e))
}
export function errorMessage(e: unknown): string {
return e instanceof Error ? e.message : String(e)
}
// Hoisted helpers (can be referenced before declaration)
function applyHeadLimit<T>(
items: T[],
limit: number | undefined,
offset: number = 0,
): { items: T[]; appliedLimit: number | undefined } {
if (limit === 0) return { items: items.slice(offset), appliedLimit: undefined }
const effectiveLimit = limit ?? DEFAULT_HEAD_LIMIT
const sliced = items.slice(offset, offset + effectiveLimit)
const wasTruncated = items.length - offset > effectiveLimit
return { items: sliced, appliedLimit: wasTruncated ? effectiveLimit : undefined }
}
function formatLimitInfo(
appliedLimit: number | undefined,
appliedOffset: number | undefined,
): string {
const parts: string[] = []
if (appliedLimit !== undefined) parts.push(`limit: ${appliedLimit}`)
if (appliedOffset) parts.push(`offset: ${appliedOffset}`)
return parts.join(', ')
}
No-Op Functions
function noop(): void {}
void for Fire-and-Forget Promises
Explicitly mark intentionally unhandled promises with void to satisfy the linter and signal intent:
void addToPromptHistory(command)
void flushPromptHistory(retries + 1)
void updateLatestDebugLogSymlink()
void resolve()
void reject(opts.abortError?.() ?? new Error('aborted'))
This pattern is used when:
- The promise result is not needed
- The failure is handled internally by the called function
- The caller should not wait for completion
Guard Clause / Early Return
Return early to avoid deep nesting. This is the preferred control flow pattern:
export function logError(error: unknown): void {
const err = toError(error)
// Guard: hard fail mode
if (feature('HARD_FAIL') && isHardFailMode()) {
console.error('[HARD FAIL]', err.stack || err.message)
process.exit(1)
}
// Guard: disabled reporting
if (
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(process.env.DISABLE_ERROR_REPORTING) ||
isEssentialTrafficOnly()
) {
return
}
// Main logic (only reached if all guards pass)
addToInMemoryErrorLog({ error: err.stack || err.message, timestamp: ... })
// ...
}
function shouldLogDebugMessage(message: string): boolean {
if (process.env.NODE_ENV === 'test' && !isDebugToStdErr()) {
return false
}
if (process.env.USER_TYPE !== 'ant' && !isDebugMode()) {
return false
}
if (typeof process === 'undefined') {
return false
}
const filter = getDebugFilter()
return shouldShowDebugMessage(message, filter)
}
Async Generators for Streaming Data
Used when data is produced incrementally or loaded lazily:
export async function* getHistory(): AsyncGenerator<HistoryEntry> {
for await (const entry of makeLogEntryReader()) {
if (!entry || typeof entry.project !== 'string') continue
yield await logEntryToHistoryEntry(entry)
}
}
// Consuming an async generator
for await (const entry of getHistory()) {
processEntry(entry)
}
Defensive Defaults
Nullish Coalescing (??)
Use ?? for null/undefined fallbacks:
const modelUsage = getUsageForModel(model) ?? {
inputTokens: 0,
outputTokens: 0,
cacheReadInputTokens: 0,
}
return TASK_ID_PREFIXES[type] ?? 'x'
const header = lines[0] ?? e.message
const title = log.agentName || log.customTitle || log.summary || ''
Default Parameter Values
export function shortErrorStack(e: unknown, maxFrames = 5): string { ... }
function applyHeadLimit<T>(
items: T[],
limit: number | undefined,
offset: number = 0,
): { ... }
Fallback Arrays
const commands = getCommands() || []
const results = await search().catch(() => [])
Module-Level Getter/Setter Pattern
For module-level mutable state, always use getter/setter functions:
let hasFormattedOutput = false
export function setHasFormattedOutput(value: boolean): void {
hasFormattedOutput = value
}
export function getHasFormattedOutput(): boolean {
return hasFormattedOutput
}
Getter/Setter with Side Effects
let systemPromptInjection: string | null = null
export function getSystemPromptInjection(): string | null {
return systemPromptInjection
}
export function setSystemPromptInjection(value: string | null): void {
systemPromptInjection = value
// Side effect: invalidate caches that depend on this value
getUserContext.cache.clear?.()
getSystemContext.cache.clear?.()
}
Lazy Loading Pattern
Commands and heavy modules use lazy loading via dynamic import():
const compact = {
type: 'local',
name: 'compact',
description: 'Clear conversation history...',
load: () => import('./compact.js'), // Only loaded when command is invoked
} satisfies Command
Conditional Require for Feature Flags
/* eslint-disable @typescript-eslint/no-require-imports */
const reactiveCompact = feature('REACTIVE_COMPACT')
? (require('../../services/compact/reactiveCompact.js') as typeof import('../../services/compact/reactiveCompact.js'))
: null
/* eslint-enable @typescript-eslint/no-require-imports */
Callback/Cleanup Registration
Register cleanup functions that run on process exit:
import { registerCleanup } from './cleanupRegistry.js'
registerCleanup(async () => {
debugWriter?.dispose()
await pendingWrite
})
Promise Chaining for Sequential Async
// Chain writes so depth stays ~1 per second
pendingWrite = pendingWrite
.then(appendAsync.bind(null, needMkdir, dir, path, content))
.catch(noop)
.bind() for Partial Application
Used to avoid closures that capture parent scope (reduces memory):
// Module-level so .bind captures only its explicit args, not the
// writeFn closure's parent scope (Jarred, #22257).
async function appendAsync(
needMkdir: boolean,
dir: string,
path: string,
content: string,
): Promise<void> { ... }
// Usage — bind instead of closure
pendingWrite = pendingWrite
.then(appendAsync.bind(null, needMkdir, dir, path, content))
.catch(noop)
setTimeout with Extra Arguments
Pass arguments directly to setTimeout instead of using closures:
// Pass resolve and signal as extra args to avoid closure
const timer = setTimeout(
(signal, onAbort, resolve) => {
signal?.removeEventListener('abort', onAbort)
void resolve()
},
ms,
signal, // arg1
onAbort, // arg2
resolve, // arg3
)
Pure Utility Functions
Small, focused functions that do one thing:
export function dateToFilename(date: Date): string {
return date.toISOString().replace(/[:.]/g, '-')
}
export function plural(count: number, word: string): string {
return count === 1 ? word : `${word}s`
}
function parseISOString(s: string): Date {
const b = s.split(/\D+/)
return new Date(Date.UTC(...))
}
Promise.race() for Timeouts
export function withTimeout<T>(
promise: Promise<T>,
ms: number,
message: string,
): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined
const timeoutPromise = new Promise<never>((_, reject) => {
timer = setTimeout(rejectWithTimeout, ms, reject, message)
if (typeof timer === 'object') timer.unref?.()
})
return Promise.race([promise, timeoutPromise]).finally(() => {
if (timer !== undefined) clearTimeout(timer)
})
}
Export Patterns
Named Exports (Most Common)
export function toError(e: unknown): Error { ... }
export const isDebugMode = memoize((): boolean => { ... })
export class ShellError extends Error { ... }
export type SessionId = string & { ... }
Default Exports (Commands Only)
// Only used for command index.ts files
export default compact
export default help
Re-Exports
export type { ToolPermissionRulesBySource }
export { inputSchema, outputSchema }