Skip to content

Latest commit

 

History

History
237 lines (232 loc) · 7.86 KB

File metadata and controls

237 lines (232 loc) · 7.86 KB
technology JavaScript
domain frontend
level Senior/Architect
version ES2024+
tags
javascript
async
promises
best-practices
clean-code
scalable-code
ai_role Senior JavaScript Asynchronous Expert
last_updated 2026-03-22

⏳ JavaScript Asynchronous & Logic Best Practices

⬆️ Back to Top

📖 Context & Scope

  • Primary Goal: Implement correct and robust asynchronous logic in JavaScript applications.
  • Target Tooling: Cursor, Windsurf, Antigravity.
  • Tech Stack Version: ES2024+

⚡ III. Asynchronous & Logic

⚡ 21. Callback Hell vs Promises

Note

Context: Managing asynchronous execution flow.

Feature Callback Hell Promises
Structure Deeply nested (Pyramid of Doom) Flattened chains (.then())
Error Handling Repetitive if (err) in every scope Centralized .catch() block
Readability Low (Hard to track flow) High (Reads sequentially)
Scalability Extremely difficult Easily composable (Promise.all)

❌ Bad Practice

getData(url, (err, res) => {
    getDetails(res.id, (err, details) => {
        saveData(details, (err, ok) => {
            // Callback Hell
        });
    });
});

⚠️ Problem

Deeply nested callbacks (the "Pyramid of Doom") make error handling extremely difficult and code unreadable.

✅ Best Practice

fetchData(url)
    .then(res => fetchDetails(res.id))
    .then(details => saveData(details))
    .catch(err => handleError(err));

🚀 Solution

Use Promises to flatten the structure and centralize error handling with .catch().

⚡ 22. Promise.then() nesting vs Async/Await

Note

Context: Modern syntax for asynchronous code.

❌ Bad Practice

function load() {
    return api.get().then(res => {
        return api.process(res).then(processed => {
            return processed;
        });
    });
}

⚠️ Problem

Even with Promises, .then() nesting can occur. It still feels like "callback style" logic.

✅ Best Practice

async function load() {
    const res = await api.get();
    const processed = await api.process(res);
    return processed;
}

🚀 Solution

Use async/await. It allows asynchronous code to be written and read like synchronous code, improving maintainability.

⚡ 23. Sequential await in loops vs Promise.all

Note

Context: Parallelizing independent asynchronous operations.

❌ Bad Practice

for (const id of ids) {
    await fetchItem(id); // Pauses loop for each request
}

⚠️ Problem

Sequential await in a loop causes a "waterfall" effect, where each request waits for the previous one to finish, significantly increasing total execution time.

✅ Best Practice

const promises = ids.map(id => fetchItem(id));
await Promise.all(promises);

🚀 Solution

Use Promise.all to execute independent promises in parallel. This utilizes the full network/IO bandwidth.

⚡ 24. Missing try/catch in async

Note

Context: Handling failures in async functions.

❌ Bad Practice

async function getData() {
    const data = await fetch(url); // If this fails, the process might crash
    return data;
}

⚠️ Problem

Unhandled exceptions in async functions result in unhandled promise rejections, which can lead to silent failures or process termination in Node.js.

✅ Best Practice

async function getData() {
    try {
        const data = await fetch(url);
        return data;
    } catch (error) {
        logError(error);
    }
}

🚀 Solution

Wrap await calls in try/catch blocks or use a higher-order function to catch errors.

⚡ 25. Floating point math errors (0.1 + 0.2)

Note

Context: Precision issues in IEEE 754 arithmetic.

❌ Bad Practice

if (0.1 + 0.2 === 0.3) { /* False! */ }

⚠️ Problem

$\text{0.1} + \text{0.2} = \text{0.30000000000000004}$ due to binary representation limits. This leads to critical bugs in financial or scientific applications.

✅ Best Practice

const EPSILON = Number.EPSILON;
const areEqual = (a, b) => Math.abs(a - b) < EPSILON;

// Or for money:
const totalCents = (10 + 20); // 30 cents

🚀 Solution

Use Number.EPSILON for comparisons or represent decimals as integers (e.g., cents instead of dollars) to avoid floating point drift.

⚡ 26. Multiple Boolean flags vs State Machine

Note

Context: Managing complex component logic.

❌ Bad Practice

const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);

⚠️ Problem

Multiple flags allow for "impossible states" (e.g., isLoading and isError both being true). This makes logic branches exponentially complex.

✅ Best Practice

const [status, setStatus] = useState('IDLE'); // IDLE, LOADING, ERROR, SUCCESS

🚀 Solution

Use a single state variable or a state machine. This ensures only one state is active at a time and simplifies transitions.

⚡ 27. Sync logic in Event Loop

Note

Context: Keeping the UI responsive.

❌ Bad Practice

function processLargeArray(arr) {
    // Blocks the main thread for 2 seconds
    arr.sort().forEach(item => complexCalc(item));
}

⚠️ Problem

JavaScript is single-threaded. Heavy synchronous computation blocks the Event Loop, causing the UI to freeze and preventing user interaction.

✅ Best Practice

// Use Web Workers or break into chunks
function processInChunks(arr) {
    if (arr.length === 0) return;
    const chunk = arr.splice(0, 100);
    process(chunk);
    setTimeout(() => processInChunks(arr), 0);
}

🚀 Solution

Offload heavy tasks to Web Workers or use requestIdleCallback/setTimeout to break long tasks into smaller chunks, allowing the browser to render between frames.

⚡ 28. Overusing classes where functions suffice

Note

Context: Paradigm choice (OOP vs FP).

❌ Bad Practice

class Calculator {
    add(a, b) { return a + b; }
}
const calc = new Calculator();

⚠️ Problem

Classes introduce unnecessary overhead (prototype chain, this binding issues) and make tree-shaking harder for bundlers.

✅ Best Practice

export const add = (a, b) => a + b;

🚀 Solution

Important

Use unambiguous functions and modules for logic. Use classes only when you need to manage complex stateful instances with shared behavior.

⚡ 29. Hard-coded Error messages vs Error Classes

Note

Context: Robust error handling and debugging.

❌ Bad Practice

throw new Error('User not found');

⚠️ Problem

Parsing error messages in catch blocks is brittle. If the string changes, the error handling logic breaks.

✅ Best Practice

class UserNotFoundError extends Error {
    constructor(userId) {
        super(`User ${userId} not found`);
        this.name = 'UserNotFoundError';
        this.code = 404;
    }
}

🚀 Solution

Extend the Error class to create custom error types. Use instanceof check in catch blocks to handle specific errors differently.

⚡ 30. Unhandled Rejections

Note

Context: Reliability of asynchronous flows.

❌ Bad Practice

// No .catch() or try/catch
fetch('/api/data');

⚠️ Problem

Unhandled rejections create silent failures. In production environments, this can lead to memory leaks as the promise state stays pending or rejected without being cleared.

✅ Best Practice

window.addEventListener('unhandledrejection', event => {
    reportToSentry(event.reason);
});

🚀 Solution

Always handle promise rejections. Implement a global unhandled rejection listener as a safety net for monitoring.