Modern JavaScript Patterns: From Callbacks to Async/Await

Comprehensive guide to modern JavaScript patterns including async/await, promises, functional programming, and practical examples for real-world applications.

S

StalkTechie

Author

September 1, 2025
1029 views

Modern JavaScript Patterns: From Callbacks to Async/Await

JavaScript has evolved from simple callback patterns to sophisticated async/await syntax with powerful functional programming concepts. In this comprehensive guide, we'll explore modern JavaScript patterns that every developer should master to write clean, efficient, and maintainable code.

The Evolution of Async JavaScript

Understanding the evolution of asynchronous JavaScript is crucial for writing modern applications. Let's explore the journey from callbacks to async/await.

1. Callback Pattern (The Old Way)


// Traditional callback pattern
function fetchData(callback) {
    setTimeout(() => {
        const data = { id: 1, name: 'John Doe', email: 'john@example.com' };
        callback(null, data);
    }, 1000);
}

// Usage with error handling
fetchData((error, data) => {
    if (error) {
        console.error('Error fetching data:', error);
        return;
    }
    console.log('Data received:', data);
    
    // Nested callbacks (callback hell)
    processData(data, (error, processed) => {
        if (error) {
            console.error('Error processing data:', error);
            return;
        }
        saveData(processed, (error, result) => {
            if (error) {
                console.error('Error saving data:', error);
                return;
            }
            console.log('Data saved successfully:', result);
        });
    });
});

// Callback with multiple operations
function fetchUserData(userId, callback) {
    // Simulate API calls
    setTimeout(() => {
        const user = { id: userId, name: 'John Doe' };
        callback(null, user);
    }, 1000);
}

function fetchUserPosts(userId, callback) {
    setTimeout(() => {
        const posts = [
            { id: 1, title: 'First Post', userId: userId },
            { id: 2, title: 'Second Post', userId: userId }
        ];
        callback(null, posts);
    }, 1500);
}

// Nested callbacks become hard to manage
fetchUserData(1, (error, user) => {
    if (error) {
        console.error('Error fetching user:', error);
        return;
    }
    
    fetchUserPosts(user.id, (error, posts) => {
        if (error) {
            console.error('Error fetching posts:', error);
            return;
        }
        
        console.log('User with posts:', { user, posts });
    });
});
    

2. Promise Pattern (ES6)


// Basic Promise implementation
function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = Math.random() > 0.3;
            if (success) {
                resolve({ id: 1, name: 'John Doe', email: 'john@example.com' });
            } else {
                reject(new Error('Failed to fetch data'));
            }
        }, 1000);
    });
}

// Usage with chaining
fetchData()
    .then(data => {
        console.log('Data received:', data);
        return processData(data); // Return a new promise
    })
    .then(processedData => {
        console.log('Processed data:', processedData);
        return saveData(processedData);
    })
    .then(result => {
        console.log('Data saved successfully:', result);
    })
    .catch(error => {
        console.error('Error in promise chain:', error);
    })
    .finally(() => {
        console.log('Operation completed (success or failure)');
    });

// Multiple parallel operations with Promise.all()
function fetchUserProfile(userId) {
    return Promise.all([
        fetchUserData(userId),
        fetchUserPosts(userId),
        fetchUserSettings(userId)
    ]).then(([user, posts, settings]) => {
        return {
            user,
            posts,
            settings
        };
    });
}

// Promise.race() for timeout patterns
function fetchWithTimeout(url, timeout = 5000) {
    const fetchPromise = fetch(url);
    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Request timeout')), timeout);
    });

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

// Promise utility functions
class PromiseUtils {
    static delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    static retry(fn, retries = 3, delay = 1000) {
        return new Promise((resolve, reject) => {
            const attempt = (attemptsLeft) => {
                fn()
                    .then(resolve)
                    .catch(error => {
                        if (attemptsLeft === 0) {
                            reject(error);
                        } else {
                            setTimeout(() => attempt(attemptsLeft - 1), delay);
                        }
                    });
            };
            attempt(retries);
        });
    }
}
    

3. Async/Await Pattern (Modern Approach)


// Basic async/await with error handling
async function fetchUserData(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const userData = await response.json();
        return userData;
    } catch (error) {
        console.error('Failed to fetch user data:', error);
        throw error; // Re-throw for caller to handle
    }
}

// Complex async operations with multiple steps
async function completeUserRegistration(userData) {
    try {
        // Step 1: Create user account
        const user = await createUserAccount(userData);
        
        // Step 2: Send welcome email
        await sendWelcomeEmail(user.email, user.name);
        
        // Step 3: Create user profile
        const profile = await createUserProfile(user.id, userData);
        
        // Step 4: Initialize user settings
        await initializeUserSettings(user.id);
        
        console.log('User registration completed successfully');
        return { user, profile };
        
    } catch (error) {
        console.error('User registration failed:', error);
        
        // Cleanup on failure
        await cleanupFailedRegistration(userData.email);
        throw error;
    }
}

// Parallel execution with async/await
async function fetchUserDashboard(userId) {
    try {
        const [user, posts, notifications, settings] = await Promise.all([
            fetch(`/api/users/${userId}`).then(r => r.json()),
            fetch(`/api/users/${userId}/posts`).then(r => r.json()),
            fetch(`/api/users/${userId}/notifications`).then(r => r.json()),
            fetch(`/api/users/${userId}/settings`).then(r => r.json())
        ]);

        return { 
            user, 
            posts, 
            notifications, 
            settings,
            lastUpdated: new Date().toISOString()
        };
    } catch (error) {
        console.error('Failed to fetch dashboard data:', error);
        throw new Error('Unable to load dashboard');
    }
}

// Advanced error handling with async/await
async function robustApiCall(url, options = {}) {
    const maxRetries = 3;
    let lastError;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const response = await fetch(url, options);
            
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            
            return await response.json();
            
        } catch (error) {
            lastError = error;
            console.warn(`Attempt ${attempt} failed:`, error.message);
            
            if (attempt < maxRetries) {
                // Exponential backoff
                const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
                await new Promise(resolve => setTimeout(resolve, delay));
            }
        }
    }
    
    throw lastError;
}
    

Functional Programming Patterns

Higher-Order Functions


// Function that returns a function (closure)
const createMultiplier = (multiplier) => (number) => number * multiplier;

const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);

console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20

// Function that takes function as argument
const users = [
    { name: 'John', age: 25, active: true },
    { name: 'Jane', age: 30, active: false },
    { name: 'Bob', age: 20, active: true },
    { name: 'Alice', age: 35, active: true }
];

// Utility functions
const getNames = users => users.map(user => user.name);
const getAdults = users => users.filter(user => user.age >= 18);
const getActiveUsers = users => users.filter(user => user.active);
const sortByAge = users => users.sort((a, b) => a.age - b.age);

// Composing functions
const getActiveAdultNames = users => 
    getNames(getAdults(getActiveUsers(users)));

console.log(getActiveAdultNames(users)); // ['John', 'Alice']

// More complex higher-order function
const createValidator = (rules) => (data) => {
    const errors = {};
    
    Object.keys(rules).forEach(field => {
        const rule = rules[field];
        const value = data[field];
        
        if (rule.required && !value) {
            errors[field] = `${field} is required`;
        }
        
        if (rule.minLength && value && value.length < rule.minLength) {
            errors[field] = `${field} must be at least ${rule.minLength} characters`;
        }
        
        if (rule.pattern && value && !rule.pattern.test(value) ) {
            errors[field] = `${field} format is invalid`;
        }
    });
    
    return {
        isValid: Object.keys(errors).length === 0,
        errors
    };
};

// Usage
const userValidator = createValidator({
    name: { required: true, minLength: 2 },
    email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
    age: { required: true }
});

const validation = userValidator({ name: 'J', email: 'invalid' });
console.log(validation);
// { isValid: false, errors: { name: 'name must be at least 2 characters', email: 'email format is invalid', age: 'age is required' } }
    

Currying and Function Composition


// Currying implementation
const curry = (fn) => {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        } else {
            return function(...args2) {
                return curried.apply(this, args.concat(args2));
            };
        }
    };
};

// Example with currying
const add = curry((a, b, c) => a + b + c);
const add5 = add(5);
const add5And10 = add5(10);

console.log(add5And10(15)); // 30
console.log(add(1)(2)(3)); // 6

// Practical currying example
const createLogger = (level) => (namespace) => (message) => {
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] [${level}] [${namespace}] ${message}`);
};

const errorLogger = createLogger('ERROR');
const apiErrorLogger = errorLogger('API');
const dbErrorLogger = errorLogger('DATABASE');

apiErrorLogger('Failed to fetch user data');
dbErrorLogger('Connection timeout');

// Function Composition
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

// Utility functions
const toUpperCase = str => str.toUpperCase();
const exclaim = str => str + '!';
const emphasize = str => `**${str}**`;
const removeSpaces = str => str.replace(/\s+/g, '');

// Composition examples
const createTitle = compose(emphasize, exclaim, toUpperCase);
const processString = pipe(removeSpaces, toUpperCase, exclaim);

console.log(createTitle('hello world')); // **HELLO WORLD!**
console.log(processString('  test  string  ')); // TESTSTRING!

// Real-world composition example
const users = [
    { name: 'john doe', age: 25, active: true },
    { name: 'jane smith', age: 17, active: true },
    { name: 'bob johnson', age: 30, active: false }
];

const capitalizeName = name => 
    name.replace(/\b\w/g, char => char.toUpperCase());

const processUsers = pipe(
    users => users.filter(user => user.active && user.age >= 18),
    users => users.map(user => ({
        ...user,
        name: capitalizeName(user.name),
        isAdult: true
    })),
    users => users.sort((a, b) => a.name.localeCompare(b.name))
);

const processedUsers = processUsers(users);
console.log(processedUsers);
// [{ name: 'Jane Smith', age: 17, active: true, isAdult: true }, ...]
    

Modern Array and Object Patterns

Advanced Array Methods


const posts = [
    { 
        id: 1, 
        title: 'JavaScript Basics', 
        views: 1500, 
        published: true, 
        tags: ['js', 'web', 'beginner'],
        author: { name: 'John Doe', verified: true },
        createdAt: new Date('2024-01-15')
    },
    { 
        id: 2, 
        title: 'Advanced React Patterns', 
        views: 890, 
        published: false, 
        tags: ['react', 'frontend', 'advanced'],
        author: { name: 'Jane Smith', verified: true },
        createdAt: new Date('2024-02-20')
    },
    { 
        id: 3, 
        title: 'Vue.js Complete Guide', 
        views: 1200, 
        published: true, 
        tags: ['vue', 'frontend', 'framework'],
        author: { name: 'Bob Johnson', verified: false },
        createdAt: new Date('2024-01-10')
    },
    { 
        id: 4, 
        title: 'Node.js Microservices', 
        views: 2100, 
        published: true, 
        tags: ['node', 'backend', 'microservices'],
        author: { name: 'Alice Brown', verified: true },
        createdAt: new Date('2024-03-05')
    }
];

// Modern array operations
const publishedPosts = posts.filter(post => post.published);
const postTitles = posts.map(post => post.title);
const totalViews = posts.reduce((sum, post) => sum + post.views, 0);
const mostViewedPost = posts.find(post => post.views > 1000);

// Advanced filtering and mapping
const popularPostTitles = posts
    .filter(post => post.published && post.views > 1000)
    .map(post => ({
        title: post.title,
        views: post.views,
        author: post.author.name
    }))
    .sort((a, b) => b.views - a.views);

// Group by tags
const postsByTag = posts.reduce((acc, post) => {
    post.tags.forEach(tag => {
        if (!acc[tag]) acc[tag] = [];
        acc[tag].push({
            id: post.id,
            title: post.title,
            author: post.author.name
        });
    });
    return acc;
}, {});

// Complex data transformation
const authorStats = posts.reduce((acc, post) => {
    const authorName = post.author.name;
    
    if (!acc[authorName]) {
        acc[authorName] = {
            totalPosts: 0,
            totalViews: 0,
            publishedPosts: 0,
            averageViews: 0,
            tags: new Set()
        };
    }
    
    acc[authorName].totalPosts++;
    acc[authorName].totalViews += post.views;
    
    if (post.published) {
        acc[authorName].publishedPosts++;
    }
    
    post.tags.forEach(tag => acc[authorName].tags.add(tag));
    
    // Calculate average
    acc[authorName].averageViews = 
        acc[authorName].totalViews / acc[authorName].totalPosts;
    
    // Convert Set to Array
    acc[authorName].tags = Array.from(acc[authorName].tags);
    
    return acc;
}, {});

console.log(authorStats);

// Array method chaining for complex queries
const recentPopularPosts = posts
    .filter(post => {
        const thirtyDaysAgo = new Date();
        thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
        return post.published && post.createdAt > thirtyDaysAgo;
    })
    .sort((a, b) => b.views - a.views)
    .slice(0, 5)
    .map(post => ({
        id: post.id,
        title: post.title,
        views: post.views,
        daysAgo: Math.floor((new Date() - post.createdAt) / (1000 * 60 * 60 * 24)),
        author: post.author.name
    }));
    

Modern Object Patterns


// Object destructuring with defaults
const user = { 
    id: 1, 
    name: 'John', 
    email: 'john@example.com', 
    age: 25,
    preferences: {
        theme: 'dark',
        notifications: true
    }
};

const { 
    name, 
    email, 
    age = 18, // default value
    preferences: { theme, notifications },
    ...rest 
} = user;

console.log(name); // John
console.log(theme); // dark
console.log(rest); // { id: 1 }

// Spread operator for objects
const userProfile = { 
    ...user, 
    avatar: 'default.jpg', 
    settings: { 
        ...user.preferences, 
        language: 'en' 
    } 
};

// Optional chaining and nullish coalescing
const theme = userProfile?.settings?.theme ?? 'light';
const postCount = user?.posts?.length ?? 0;
const userName = user?.name ?? 'Anonymous';

// Dynamic property names
const createUser = (id, data) => ({
    id,
    [`user_${id}`]: data,
    createdAt: new Date().toISOString(),
    ...data
});

const newUser = createUser(123, { name: 'John', age: 25 });
console.log(newUser.user_123); // { name: 'John', age: 25 }

// Object transformation
const userMap = posts.reduce((acc, post) => {
    const author = post.author.name;
    if (!acc[author]) {
        acc[author] = [];
    }
    acc[author].push({
        title: post.title,
        views: post.views,
        published: post.published
    });
    return acc;
}, {});

// Converting object to array for processing
const authorArray = Object.entries(userMap).map(([author, posts]) => ({
    author,
    postCount: posts.length,
    totalViews: posts.reduce((sum, post) => sum + post.views, 0),
    averageViews: posts.reduce((sum, post) => sum + post.views, 0) / posts.length
}));
    

Utility Patterns and Best Practices

Debounce and Throttle


// Advanced debounce with immediate execution option
function debounce(func, wait, immediate = false) {
    let timeout;
    let result;
    
    return function executedFunction(...args) {
        const context = this;
        
        const later = () => {
            timeout = null;
            if (!immediate) {
                result = func.apply(context, args);
            }
        };
        
        const callNow = immediate && !timeout;
        
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        
        if (callNow) {
            result = func.apply(context, args);
        }
        
        return result;
    };
}

// Throttle implementation
function throttle(func, limit) {
    let inThrottle;
    let lastResult;
    
    return function(...args) {
        const context = this;
        
        if (!inThrottle) {
            lastResult = func.apply(context, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
        
        return lastResult;
    };
}

// Usage in search input with API calls
const searchInput = document.getElementById('search');
const performSearch = debounce(async function(query) {
    if (!query.trim()) {
        displayResults([]);
        return;
    }
    
    try {
        const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
        if (!response.ok) throw new Error('Search failed');
        
        const results = await response.json();
        displayResults(results);
    } catch (error) {
        console.error('Search error:', error);
        displayError('Failed to perform search');
    }
}, 300);

searchInput.addEventListener('input', (e) => {
    performSearch(e.target.value);
});

// Throttle for scroll events
const handleScroll = throttle(function() {
    const scrollPosition = window.scrollY;
    const windowHeight = window.innerHeight;
    const documentHeight = document.documentElement.scrollHeight;
    
    if (scrollPosition + windowHeight >= documentHeight - 100) {
        loadMoreContent();
    }
}, 200);

window.addEventListener('scroll', handleScroll);
    

Memoization Pattern


// Advanced memoization with cache expiration
function memoize(fn, options = {}) {
    const cache = new Map();
    const { ttl = 0 } = options; // Time to live in milliseconds
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache.has(key)) {
            const { value, timestamp } = cache.get(key);
            
            // Check if cache has expired
            if (ttl > 0 && (Date.now() - timestamp) > ttl) {
                cache.delete(key);
            } else {
                return value;
            }
        }
        
        const result = fn.apply(this, args);
        cache.set(key, {
            value: result,
            timestamp: Date.now()
        });
        
        // Optional: Limit cache size
        if (cache.size > 100) {
            const firstKey = cache.keys().next().value;
            cache.delete(firstKey);
        }
        
        return result;
    };
}

// Expensive calculation with memoization
const fibonacci = memoize(function(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}, { ttl: 60000 }); // Cache for 1 minute

console.log(fibonacci(40)); // Calculated once, cached for subsequent calls

// API call memoization
const fetchUserData = memoize(async function(userId) {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error('User not found');
    return response.json();
}, { ttl: 300000 }); // Cache for 5 minutes

// Usage
async function getUserProfile(userId) {
    try {
        const userData = await fetchUserData(userId);
        return userData;
    } catch (error) {
        console.error('Failed to fetch user:', error);
        throw error;
    }
}
    

Best Practice:

Always use const for variables that won't be reassigned, and let for those that will. Avoid var in modern JavaScript code. Use descriptive variable names and follow consistent naming conventions.

Error Handling Patterns


// Custom error classes for better error handling
class NetworkError extends Error {
    constructor(message, statusCode, url) {
        super(message);
        this.name = 'NetworkError';
        this.statusCode = statusCode;
        this.url = url;
        this.timestamp = new Date().toISOString();
    }
    
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            statusCode: this.statusCode,
            url: this.url,
            timestamp: this.timestamp,
            stack: this.stack
        };
    }
}

class ValidationError extends Error {
    constructor(message, field, value) {
        super(message);
        this.name = 'ValidationError';
        this.field = field;
        this.value = value;
    }
}

// Robust error handling with retry logic
async function fetchWithRetry(url, options = {}, retries = 3) {
    let lastError;
    
    for (let i = 0; i < retries; i++) {
        try {
            const response = await fetch(url, options);
            
            if (!response.ok) {
                throw new NetworkError(
                    `HTTP ${response.status}: ${response.statusText}`,
                    response.status,
                    url
                );
            }
            
            return await response.json();
            
        } catch (error) {
            lastError = error;
            
            if (i === retries - 1) break;
            
            // Exponential backoff
            const delay = Math.min(1000 * Math.pow(2, i), 10000);
            console.warn(`Attempt ${i + 1} failed, retrying in ${delay}ms:`, error.message);
            
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
    
    throw lastError;
}

// Error boundary for React-like error handling
class ErrorBoundary {
    constructor(operation) {
        this.operation = operation;
    }
    
    async execute(...args) {
        try {
            return await this.operation(...args);
        } catch (error) {
            this.handleError(error);
            throw error;
        }
    }
    
    handleError(error) {
        console.error('Operation failed:', error);
        
        // Send to error tracking service
        this.reportError(error);
        
        // Show user-friendly message
        this.showUserMessage(error);
    }
    
    reportError(error) {
        // Integrate with error tracking service like Sentry
        if (typeof window !== 'undefined' && window.Sentry) {
            window.Sentry.captureException(error);
        }
    }
    
    showUserMessage(error) {
        let message = 'An unexpected error occurred';
        
        if (error instanceof NetworkError) {
            message = 'Network error. Please check your connection.';
        } else if (error instanceof ValidationError) {
            message = `Invalid ${error.field}: ${error.message}`;
        }
        
        // Show notification to user
        this.displayNotification(message, 'error');
    }
    
    displayNotification(message, type) {
        // Implementation depends on your UI framework
        console.log(`[${type.toUpperCase()}] ${message}`);
    }
}

// Usage
const apiCall = new ErrorBoundary(async (userId) => {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new NetworkError('User not found', 404, `/api/users/${userId}`);
    return response.json();
});

// Safe execution with error handling
async function loadUserData(userId) {
    try {
        const user = await apiCall.execute(userId);
        return user;
    } catch (error) {
        // Error already handled by ErrorBoundary
        return null;
    }
}
    

Modern Module Patterns


// ES6 Modules with named and default exports
// utils/arrayHelpers.js
export const chunkArray = (array, size) => {
    const chunks = [];
    for (let i = 0; i < array.length; i += size) {
        chunks.push(array.slice(i, i + size));
    }
    return chunks;
};

export const uniqueBy = (array, key) => {
    const seen = new Set();
    return array.filter(item => {
        const value = item[key];
        if (seen.has(value)) {
            return false;
        }
        seen.add(value);
        return true;
    });
};

export const groupBy = (array, key) => {
    return array.reduce((groups, item) => {
        const group = item[key];
        if (!groups[group]) groups[group] = [];
        groups[group].push(item);
        return groups;
    }, {});
};

export default {
    chunkArray,
    uniqueBy,
    groupBy
};

// api/userService.js
import { makeRequest } from './httpClient';

export class UserService {
    constructor(baseURL = '/api') {
        this.baseURL = baseURL;
    }
    
    async getUser(id) {
        return makeRequest(`${this.baseURL}/users/${id}`);
    }
    
    async createUser(userData) {
        return makeRequest(`${this.baseURL}/users`, {
            method: 'POST',
            body: JSON.stringify(userData)
        });
    }
    
    async updateUser(id, updates) {
        return makeRequest(`${this.baseURL}/users/${id}`, {
            method: 'PUT',
            body: JSON.stringify(updates)
        });
    }
    
    async deleteUser(id) {
        return makeRequest(`${this.baseURL}/users/${id}`, {
            method: 'DELETE'
        });
    }
}

export default new UserService();

// Usage in main application
import UserService, { UserService as UserServiceClass } from './api/userService';
import { chunkArray, groupBy } from './utils/arrayHelpers';

async function initializeApp() {
    try {
        const user = await UserService.getUser(1);
        const users = await UserService.getUsers();
        
        const chunkedUsers = chunkArray(users, 10);
        const usersByRole = groupBy(users, 'role');
        
        console.log('App initialized successfully');
    } catch (error) {
        console.error('Failed to initialize app:', error);
    }
}
    

These modern JavaScript patterns will help you write more maintainable, efficient, and robust code. Practice them in your projects to become a more effective JavaScript developer and build applications that scale gracefully with complexity.

Share this post:

Related Articles

Discussion

0 comments

Please log in to join the discussion.

Login to Comment