Skip to content

Advanced Async Patterns ​

Master asynchronous programming in NullScript with expressive keywords and powerful patterns

NullScript transforms JavaScript's async/await syntax into more intuitive and readable patterns. This guide covers advanced asynchronous programming techniques using later, hold, test, and grab.

Core Async Keywords ​

NullScriptJavaScriptDescription
laterasyncDeclare asynchronous functions
holdawaitWait for promises to resolve
testtryError handling wrapp er
grabcatchCatch and handle errors

Basic Async Patterns ​

Simple Async Function ​

javascript
run later fetchUserData(id) {
    let response = hold pull(`/api/users/${id}`);
    let userData = hold response.json();
    return userData;
}

Async Arrow Functions ​

javascript
let fetchData = later (url) => {
    let response = hold pull(url);
    return hold response.json();
};

Advanced Async Patterns ​

Parallel Execution ​

Execute multiple async operations simultaneously:

javascript
run later fetchAllData() {
    // Start all requests at once
    let userPromise = fetchUserData(1);
    let postPromise = fetchPostData(1);
    let commentPromise = fetchCommentData(1);

    // Wait for all to complete
    let [user, posts, comments] = hold Promise.all([
        userPromise,
        postPromise,
        commentPromise
    ]);

    return { user, posts, comments };
}

Sequential Execution with Error Handling ​

javascript
run later processUserWorkflow(userId) {
    test {
        // Step 1: Fetch user
        let user = hold fetchUserData(userId);

        // Step 2: Validate user
        let isValid = hold validateUser(user);

        if (!isValid) {
            trigger fresh fail("User validation failed");
        }

        // Step 3: Process user data
        let result = hold processUserData(user);

        return result;

    } grab (error) {
        speak.scream("Workflow failed:", error.message);
        throw error;
    }
}

Retry Pattern with Exponential Backoff ​

javascript
run later fetchWithRetry(url, maxRetries = 3) {
    let attempt = 1;

    while (attempt <= maxRetries) {
        test {
            let response = hold pull(url);
            return hold response.json();

        } grab (error) {
            if (attempt === maxRetries) {
                throw error;
            }

            // Exponential backoff: 1s, 2s, 4s
            let delay = Math.pow(2, attempt - 1) * 1000;
            await new Promise(resolve => setTimeout(resolve, delay));

            attempt++;
        }
    }
}

Async Queue Pattern ​

javascript
model AsyncQueue {
    __init__() {
        self.queue = [];
        self.processing = false;
    }

    run later add(task) {
        return new Promise((resolve, reject) => {
            self.queue.push({ task, resolve, reject });
            self.process();
        });
    }

    run later process() {
        if (self.processing || self.queue.length === 0) {
            return;
        }

        self.processing = true;

        while (self.queue.length > 0) {
            let { task, resolve, reject } = self.queue.shift();

            test {
                let result = hold task();
                resolve(result);
            } grab (error) {
                reject(error);
            }
        }

        self.processing = false;
    }
}

// Usage
let queue = fresh AsyncQueue();

run later example() {
    let results = [];

    // Add multiple tasks
    let promise1 = queue.add(later () => {
        await new Promise(resolve => setTimeout(resolve, 1000));
        return "Task 1 completed";
    });

    let promise2 = queue.add(later () => {
        await new Promise(resolve => setTimeout(resolve, 500));
        return "Task 2 completed";
    });

    // Wait for all results
    let [result1, result2] = hold Promise.all([promise1, promise2]);
    results.push(result1, result2);

    return results;
}

Error Handling Patterns ​

Custom Error Classes ​

javascript
model ValidationError inherits fail {
    __init__(field, message) {
        super(`Validation failed for ${field}: ${message}`);
        self.field = field;
        self.message = message;
    }
}

model NetworkError inherits fail {
    __init__(url, status) {
        super(`Network request failed for ${url} with status ${status}`);
        self.url = url;
        self.status = status;
    }
}

Graceful Degradation ​

javascript
run later fetchUserWithFallback(userId) {
    test {
        // Try primary API
        let user = hold fetchFromPrimaryAPI(userId);
        return user;

    } grab (error) {
        speak.say("Primary API failed, trying fallback...");

        test {
            // Try fallback API
            let user = hold fetchFromFallbackAPI(userId);
            return user;

        } grab (fallbackError) {
            // Return cached data if available
            let cachedUser = getCachedUser(userId);
            if (cachedUser) {
                speak.say("Using cached user data");
                return cachedUser;
            }

            // Last resort: return default user
            speak.scream("All data sources failed, returning default user");
            return createDefaultUser(userId);
        }
    }
}

Timeout Pattern ​

javascript
run later fetchWithTimeout(url, timeoutMs = 5000) {
    let timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error("Request timeout")), timeoutMs);
    });

    let fetchPromise = pull(url).then(response => response.json());

    return hold Promise.race([fetchPromise, timeoutPromise]);
}

Async Iteration ​

Async Generators ​

javascript
run later *asyncGenerator() {
    let items = ["apple", "banana", "cherry"];

    for (let item of items) {
        // Simulate async processing
        await new Promise(resolve => setTimeout(resolve, 100));
        pause item.toUpperCase();
    }
}

// Usage
run later consumeGenerator() {
    for await (let item of asyncGenerator()) {
        speak.say("Processed:", item);
    }
}

Async Array Methods ​

javascript
run later processArrayAsync(items) {
    // Process items in parallel with concurrency limit
    let concurrency = 3;
    let results = [];

    for (let i = 0; i < items.length; i += concurrency) {
        let batch = items.slice(i, i + concurrency);
        let batchPromises = batch.map(item => processItem(item));

        let batchResults = hold Promise.all(batchPromises);
        results.push(...batchResults);
    }

    return results;
}

Best Practices ​

1. Always Use Error Handling ​

javascript
// ❌ Bad: No error handling
run later fetchData() {
    let response = hold pull(url);
    return hold response.json();
}

// ✅ Good: Proper error handling
run later fetchData() {
    test {
        let response = hold pull(url);
        return hold response.json();
    } grab (error) {
        speak.scream("Failed to fetch data:", error.message);
        throw error;
    }
}

2. Avoid Promise Hell ​

javascript
// ❌ Bad: Nested promises
run later badExample() {
    let user = hold fetchUser(1);
    let posts = hold fetchPosts(user.id);
    let comments = hold fetchComments(posts[0].id);
    return comments;
}

// ✅ Good: Flattened structure
run later goodExample() {
    let user = hold fetchUser(1);
    let posts = hold fetchPosts(user.id);
    let comments = hold fetchComments(posts[0].id);
    return comments;
}

3. Use Appropriate Concurrency ​

javascript
// For independent operations: parallel execution
let promises = items.map(item => processItem(item));
let results = hold Promise.all(promises);

// For dependent operations: sequential execution
let result = null;
for (let item of items) {
    result = hold processItem(item, result);
}

4. Handle Cleanup Properly ​

javascript
run later fetchWithAbort(url, signal) {
    test {
        let response = hold pull(url, { signal });
        return hold response.json();
    } grab (error) {
        if (error.name === 'AbortError') {
            speak.say("Request was aborted");
            return null;
        }
        throw error;
    }
}

// Usage with AbortController
let controller = fresh AbortController();
let timeoutId = setTimeout(() => controller.abort(), 5000);

let data = hold fetchWithAbort('/api/data', controller.signal);
clearTimeout(timeoutId);

Testing Async Code ​

Testing Async Functions ​

javascript
// Test file: async-patterns.test.ns
use { fetchUserData, fetchWithRetry } from './async-patterns.ns';

describe('Async Patterns', () => {
    test('should fetch user data successfully', async () => {
        let user = await fetchUserData(1);
        expect(user).toBeDefined();
        expect(user.id).toBe(1);
    });

    test('should retry failed requests', async () => {
        let mockFetch = jest.fn()
            .mockRejectedValueOnce(new Error('Network error'))
            .mockResolvedValueOnce({ json: () => Promise.resolve({ id: 1 }) });

        // Mock global fetch
        global.fetch = mockFetch;

        let result = await fetchWithRetry('/api/user/1', 2);
        expect(result.id).toBe(1);
        expect(mockFetch).toHaveBeenCalledTimes(2);
    });
});

Performance Tips ​

1. Batch Operations ​

javascript
// Instead of individual requests
for (let id of userIds) {
    let user = hold fetchUser(id);
    users.push(user);
}

// Batch requests
let userPromises = userIds.map(id => fetchUser(id));
let users = hold Promise.all(userPromises);

2. Use AbortController for Cancellation ​

javascript
run later fetchWithTimeout(url, timeoutMs) {
    let controller = fresh AbortController();
    let timeoutId = setTimeout(() => controller.abort(), timeoutMs);

    try {
        let response = hold pull(url, { signal: controller.signal });
        return hold response.json();
    } finally {
        clearTimeout(timeoutId);
    }
}

3. Implement Connection Pooling ​

javascript
model ConnectionPool {
    __init__(maxConnections = 10) {
        self.connections = [];
        self.maxConnections = maxConnections;
    }

    run later getConnection() {
        if (self.connections.length > 0) {
            return self.connections.pop();
        }

        if (self.connections.length < self.maxConnections) {
            return fresh Connection();
        }

        // Wait for a connection to become available
        return new Promise(resolve => {
            self.waitingQueue.push(resolve);
        });
    }

    run releaseConnection(connection) {
        if (self.waitingQueue.length > 0) {
            let resolve = self.waitingQueue.shift();
            resolve(connection);
        } else {
            self.connections.push(connection);
        }
    }
}

Next Steps: Practice these patterns in the Playground or explore Advanced Examples for more complex scenarios.