🎯

async-over-callbacks

🎯Skill

from marius-townhouse/effective-typescript-skills

VibeIndex|
What it does

Transforms callback-based asynchronous code into clean, readable async/await patterns for better type flow and error handling.

πŸ“¦

Part of

marius-townhouse/effective-typescript-skills(83 items)

async-over-callbacks

Installation

Quick InstallInstall with npx
npx skills add marius-townhouse/effective-typescript-skills --all
Quick InstallInstall with npx
npx skills add marius-townhouse/effective-typescript-skills -s prefer-unknown-over-any exhaustiveness-checking
Quick InstallInstall with npx
npx skills add marius-townhouse/effective-typescript-skills -a opencode claude-code
Quick InstallInstall with npx
npx skills add marius-townhouse/effective-typescript-skills -l
git cloneClone repository
git clone https://github.com/marius-townhouse/effective-typescript-skills.git
πŸ“– Extracted from docs: marius-townhouse/effective-typescript-skills
1Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Use when writing asynchronous code. Use when tempted to use callbacks. Use when composing multiple async operations.

Overview

# Use async Functions Instead of Callbacks

Overview

Prefer async/await over callbacks for cleaner code and better type flow.

Callbacks create nested, hard-to-follow code. Promises and async/await flatten the structure, make types flow naturally, and enable better error handling.

When to Use This Skill

  • Writing any asynchronous code
  • Tempted to use callback-style APIs
  • Chaining multiple async operations
  • Need to compose concurrent operations
  • Working with APIs that return Promises

The Iron Rule

```

ALWAYS prefer async/await over callbacks for new code.

```

Remember:

  • async/await is syntactic sugar over Promises
  • Types flow through Promises automatically
  • Error handling is cleaner with try/catch
  • Concurrent operations compose easily

Detection: The Callback Pyramid

If you see nested callbacks (the "pyramid of doom"), refactor to async/await:

```typescript

// ❌ Callback hell - hard to read, types don't flow well

fetchURL(url1, function(response1) {

fetchURL(url2, function(response2) {

fetchURL(url3, function(response3) {

// ... deeply nested

console.log(1);

});

console.log(2);

});

console.log(3);

});

console.log(4);

// Logs: 4, 3, 2, 1 (confusing order!)

```

The async/await Solution

```typescript

// βœ… Clean, flat, readable

async function fetchPages() {

const response1 = await fetch(url1);

const response2 = await fetch(url2);

const response3 = await fetch(url3);

// Execution order matches code order

}

```

Why Types Flow Better

Callbacks Require Manual Type Annotations

```typescript

// ❌ Callbacks - you must annotate types

function fetchUser(

id: string,

callback: (user: User | null, error?: Error) => void

) {

// ...

}

fetchUser('123', (user, error) => {

if (error) { / handle / }

if (user) { / use user / }

});

```

Promises Carry Types Automatically

```typescript

// βœ… Promises - types flow through

async function fetchUser(id: string): Promise {

const response = await fetch(/api/users/${id});

return response.json(); // TypeScript knows this returns Promise

}

const user = await fetchUser('123');

// ^? const user: User

```

Composing Async Operations

Sequential Operations

```typescript

async function getFullUserData(id: string) {

const user = await fetchUser(id);

const posts = await fetchPosts(user.id);

const comments = await fetchComments(posts);

return { user, posts, comments };

}

// Return type is automatically inferred

```

Concurrent Operations with Promise.all

```typescript

async function fetchAllPages() {

// Run all fetches concurrently, wait for all to complete

const [page1, page2, page3] = await Promise.all([

fetch(url1),

fetch(url2),

fetch(url3),

]);

// Types are inferred: [Response, Response, Response]

}

```

Race Conditions with Promise.race

```typescript

async function fetchWithTimeout(url: string, ms: number) {

return Promise.race([

fetch(url),

new Promise((_, reject) =>

setTimeout(() => reject(new Error('Timeout')), ms)

),

]);

}

```

Error Handling

Callbacks: Error-First Convention (Manual)

```typescript

// ❌ Manual error handling, easy to forget

fetchData(url, (error, data) => {

if (error) {

console.error(error);

return;

}

// use data

});

```

async/await: Natural try/catch

```typescript

// βœ… Standard exception handling

async function fetchData(url: string) {

try {

const response = await fetch(url);

if (!response.ok) {

throw new Error(HTTP ${response.status});

}

return await response.json();

} catch (error) {

console.error('Fetch failed:', error);

throw error; // Re-throw or handle

}

}

```

Common Patterns

Always Return Promises from async Functions

```typescript

// ❌ Inconsistent return type

function getQuote(ticker: string) {

if (cache[ticker]) {

return cache[ticker]; // Returns number

}

return fetch(/quote?t=${ticker})

.then(r => r.json()); // Returns Promise

}

// Type: number | Promise - confusing!

// βœ… Consistent Promise return

async function getQuote(ticker: string): Promise {

if (cache[ticker]) {

return cache[ticker]; // Automatically wrapped in Promise

}

const response = await fetch(/quote?t=${ticker});

return response.json();

}

```

Annotate Return Types for Clarity

```typescript

// βœ… Explicit return type catches mistakes

async function fetchUser(id: string): Promise {

const response = await fetch(/api/users/${id});

// TypeScript ensures we return a User

return response.json();

}

```

Use Promise.allSettled for Partial Failures

```typescript

async function fetchAllUsers(ids: string[]) {

const results = await Promise.allSettled(

ids.map(id => fetchUser(id))

);

// Handle both successes and failures

const users: User[] = [];

for (const result of results) {

if (result.status === 'fulfilled') {

users.push(result.value);

} else {

console.error('Failed:', result.reason);

}

}

return users;

}

```

Converting Callbacks to Promises

Using util.promisify (Node.js)

```typescript

import { promisify } from 'util';

import { readFile } from 'fs';

const readFileAsync = promisify(readFile);

async function loadConfig() {

const data = await readFileAsync('config.json', 'utf-8');

return JSON.parse(data);

}

```

Manual Promisification

```typescript

function fetchURLAsync(url: string): Promise {

return new Promise((resolve, reject) => {

fetchURL(url, (response, error) => {

if (error) {

reject(error);

} else {

resolve(response);

}

});

});

}

```

Immediate Execution in async Functions

```typescript

// async functions return a Promise, even with immediate values

async function getValue(): Promise {

return 42; // Wrapped in Promise.resolve(42)

}

// To unwrap, you must await

const value = await getValue();

// ^? const value: number

```

Pressure Resistance Protocol

1. "Callbacks Are Faster"

Pressure: "Promises have overhead, callbacks are more performant"

Response: The overhead is negligible. Code clarity and type safety matter more.

Action: Use async/await. Profile if you suspect performance issues.

2. "The API Only Supports Callbacks"

Pressure: "This library uses callbacks, we have to use them too"

Response: Wrap callback APIs in Promises.

Action: Use promisify or create a Promise wrapper.

3. "We're Already Using Callbacks Everywhere"

Pressure: "Consistency with existing code"

Response: Gradually migrate. New code should use async/await.

Action: Wrap old APIs, write new code with async/await.

Red Flags - STOP and Reconsider

  • Nested callbacks (pyramid of doom)
  • Functions that sometimes return values, sometimes Promises
  • Manual error handling with error-first callbacks
  • Mixing async/await with .then() chains unnecessarily

Common Rationalizations (All Invalid)

| Excuse | Reality |

|--------|---------|

| "Callbacks are simpler" | async/await is more readable and maintainable |

| "Promise overhead is too high" | Negligible in practice, clarity wins |

| "Our team knows callbacks" | async/await is standard modern JavaScript |

Quick Reference

| Pattern | Callbacks | async/await |

|---------|-----------|-------------|

| Sequential ops | Nested callbacks | Sequential await |

| Concurrent ops | Manual tracking | Promise.all |

| Error handling | Error-first convention | try/catch |

| Type inference | Manual annotations | Automatic flow |

| Composition | Difficult | Natural |

The Bottom Line

async/await produces cleaner code with better type inference.

Callbacks create pyramids of nested code where types don't flow well. Promises and async/await flatten the structure, compose naturally, handle errors with standard try/catch, and let TypeScript infer types automatically. Use async/await for all new async code.

Reference

Based on "Effective TypeScript" by Dan Vanderkam, Item 27: Use async Functions Instead of Callbacks to Improve Type Flow.

More from this repository10

🎯
tsdoc-comments🎯Skill

Generates TypeScript documentation comments (TSDoc) to explain public APIs, complex types, and provide comprehensive code documentation with IDE tooltips.

🎯
code-gen-independent🎯Skill

Generates JavaScript code despite TypeScript type errors and demonstrates that TypeScript types are erased at runtime, requiring alternative type checking strategies.

🎯
context-type-inference🎯Skill

Helps restore precise type context when extracting values, preventing type inference errors through annotations, const assertions, and type preservation techniques.

🎯
module-by-module-migration🎯Skill

Guides developers through systematic TypeScript module migration, breaking down complex refactoring into manageable, incremental steps.

🎯
ts-js-relationship🎯Skill

Explains TypeScript's relationship to JavaScript, highlighting how it adds static typing and catches errors before runtime while remaining fully compatible with JavaScript code.

🎯
type-safe-monkey-patching🎯Skill

Enables type-safe runtime extension of global objects and DOM elements in TypeScript without sacrificing type checking or using `as any`.

🎯
create-objects-all-at-once🎯Skill

Efficiently initializes multiple TypeScript objects simultaneously using concise object literal syntax and spread operators.

🎯
precise-string-types🎯Skill

Enforces strict string type constraints and prevents unintended string type conversions in TypeScript projects.

🎯
variadic-tuple-types🎯Skill

Demonstrates advanced TypeScript tuple type techniques for creating flexible function signatures with varying argument lengths and types.

🎯
type-display-attention🎯Skill

Displays and simplifies complex TypeScript types to improve IDE readability and developer experience.