#1659·es-toolkit

Feature request: `call` / `callAsync`

Author: zrosenbauerCreated Mar 19, 2026Updated Jun 23, 2026

Problem

JavaScript has no expression-oriented blocks. When computing a value through conditionals or multiple steps, you're stuck with either mutable let bindings or IIFEs:

typescript
// Mutable — result is `let` for the rest of the scope
let result: string;
if (user.role === "admin") {
  result = formatPerms(await fetchAdminPerms(user.id));
} else {
  result = "none";
}

// IIFE — noisy, hard to scan
const result = (async () => {
  if (user.role === "admin") return formatPerms(await fetchAdminPerms(user.id));
  return "none";
})();

IIFEs are widely considered a code smell in modern JavaScript:

Examples in other languages

Language Feature Example
Kotlin run { } val x = run { ... }
Rust Block expressions let x = { ... };
Scala Block expressions val x = { ... }
Ruby Inline blocks x = begin ... end

Proposal

typescript
// Sync
const result = call(() => {
  if (user.role === "admin") return "full";
  if (user.role === "editor") return "partial";
  return "none";
});

// Async
const result = await callAsync(async () => {
  const perms = await fetchPerms(user.id);
  const roles = await fetchRoles(user.id);
  return merge(perms, roles);
});

Implementation:

typescript
export function call<R>(fn: () => R): R {
  return fn();
}

export async function callAsync<R>(fn: () => Promise<R>): Promise<R> {
  return await fn();
}

The implementation is trivial — but so are noop, identity, and attempt. The value is naming the pattern, not the complexity of the code. call(() => ...) communicates intent where (() => ...)() hides it in punctuation.

Precedent

es-toolkit already wraps simple patterns into named utilities:

  • attempt(fn) — wraps try/catch
  • noop() — wraps () => {}
  • identity(x) — wraps (x) => x

call(fn) fits the same category.