Advanced JavaScript - Promises, Network (API), Storage

Code SubtleCode Subtle
10 min read

๐ŸŒŸ Intro to Promises

โœ… Definition:

A Promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value. Promises provide a cleaner way to handle asynchronous code compared to callbacks, avoiding callback hell. They have three states: pending, fulfilled (resolved), or rejected, and allow chaining of operations.

๐Ÿ“Œ Syntax:

const promise = new Promise((resolve, reject) => {
    // Asynchronous operation
    if (success) resolve(value);
    else reject(error);
});

๐Ÿงช Example:

const orderPizza = new Promise((resolve, reject) => {
    let pizzaReady = true;

    setTimeout(() => {
        if (pizzaReady) {
            resolve("Pizza is ready! ๐Ÿ•");
        } else {
            reject("Pizza could not be made ๐Ÿ˜ž");
        }
    }, 2000);
});

orderPizza
    .then(message => console.log(message))
    .catch(error => console.log(error));

โšก Microtask Queue

โœ… Definition:

The microtask queue is a special queue that has higher priority than the regular callback queue (task queue) in JavaScript's event loop. Promise callbacks (.then, .catch, .finally) are placed in the microtask queue and are executed before any tasks from the callback queue. This ensures that promise resolutions are handled immediately after the current execution stack is empty.

๐Ÿ“Œ Syntax:

// Microtasks (higher priority)
Promise.resolve().then(() => console.log('Microtask'));

// Macrotasks (lower priority)
setTimeout(() => console.log('Macrotask'), 0);

๐Ÿงช Example:

console.log('1'); // Synchronous

setTimeout(() => console.log('2 - setTimeout'), 0); // Macrotask

Promise.resolve().then(() => console.log('3 - Promise')); // Microtask

console.log('4'); // Synchronous

// Output: 1, 4, 3 - Promise, 2 - setTimeout

๐Ÿ”„ Function that Returns Promise

โœ… Definition:

A function that returns a promise allows you to create reusable asynchronous operations that can be chained and handled consistently. These functions encapsulate async logic and return a promise object that resolves or rejects based on the operation's outcome. This pattern enables better code organization and reusability for async operations.

๐Ÿ“Œ Syntax:

function asyncFunction() {
    return new Promise((resolve, reject) => {
        // Async operation logic
    });
}

๐Ÿงช Example:

function fetchUserData(userId) {
    return new Promise((resolve, reject) => {
        const users = { 1: 'Alice', 2: 'Bob', 3: 'Charlie' };

        setTimeout(() => {
            if (users[userId]) {
                resolve({ id: userId, name: users[userId] });
            } else {
                reject(`User with ID ${userId} not found`);
            }
        }, 1000);
    });
}

// Usage
fetchUserData(1)
    .then(user => console.log('User:', user.name))
    .catch(error => console.log('Error:', error));

โฐ Promise and setTimeout

โœ… Definition:

Combining promises with setTimeout allows you to create delayed asynchronous operations that resolve or reject after a specified time period. This combination is useful for simulating API calls, creating delays in async flows, or implementing timeout functionality. The setTimeout function runs in the background while the promise provides a clean interface for handling the delayed result.

๐Ÿ“Œ Syntax:

function delay(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(), ms);
    });
}

๐Ÿงช Example:

function delayedMessage(message, delay) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (message) {
                resolve(`Message: ${message}`);
            } else {
                reject('No message provided');
            }
        }, delay);
    });
}

delayedMessage('Hello World!', 2000)
    .then(result => console.log(result))
    .catch(error => console.log(error));

// Output after 2 seconds: Message: Hello World!

โœจ Promise.resolve and More About then Method

โœ… Definition:

Promise.resolve() creates a promise that is immediately resolved with the given value, useful for converting non-promise values into promises. The .then() method always returns a new promise, enabling method chaining and allowing you to transform values through multiple steps. If you return a value from .then(), it's automatically wrapped in a resolved promise.

๐Ÿ“Œ Syntax:

Promise.resolve(value).then(result => newValue);
// Chain multiple .then() calls
promise.then(value => transform1).then(value => transform2);

๐Ÿงช Example:

// Promise.resolve example
Promise.resolve(5)
    .then(value => {
        console.log('Initial value:', value); // 5
        return value * 2;
    })
    .then(value => {
        console.log('Doubled value:', value); // 10
        return value + 10;
    })
    .then(value => {
        console.log('Final value:', value); // 20
    });

// Chaining with string manipulation
Promise.resolve('hello')
    .then(str => str + ' world')
    .then(str => str.toUpperCase())
    .then(str => console.log(str)); // HELLO WORLD

๐Ÿ”ง Convert Nested Callbacks to Flat Code Using Promises

โœ… Definition:

Converting callback hell to promises involves replacing nested callback functions with promise chains using .then() methods. This transformation eliminates the pyramid-shaped indentation and makes error handling more consistent with .catch(). Promise chaining creates a linear, readable flow that's easier to maintain and debug than deeply nested callbacks.

๐Ÿ“Œ Syntax:

// Instead of: callback1(() => callback2(() => callback3()))
// Use: promise1().then(() => promise2()).then(() => promise3())

๐Ÿงช Example:

// โŒ Callback Hell
function callbackHell() {
    getUser(123, function(err, user) {
        if (err) throw err;
        getUserPosts(user.id, function(err, posts) {
            if (err) throw err;
            getComments(posts[0], function(err, comments) {
                if (err) throw err;
                console.log(comments);
            });
        });
    });
}

// โœ… Promise Chain
function promiseChain() {
    return getUser(123)
        .then(user => getUserPosts(user.id))
        .then(posts => getComments(posts[0]))
        .then(comments => console.log(comments))
        .catch(error => console.error('Error:', error));
}

// Promise-based functions
function getUser(id) {
    return Promise.resolve({ id, name: 'Alice' });
}

function getUserPosts(userId) {
    return Promise.resolve([{ id: 1, title: 'Post 1' }]);
}

function getComments(post) {
    return Promise.resolve(['Great post!', 'Thanks for sharing']);
}

๐ŸŒ Intro to Ajax, HTTP Request

โœ… Definition:

AJAX (Asynchronous JavaScript and XML) is a technique for making asynchronous HTTP requests to servers without refreshing the entire web page. HTTP requests allow web applications to communicate with servers to send and receive data in various formats like JSON, XML, or plain text. Modern JavaScript uses the Fetch API or libraries like Axios to make these requests more efficiently than the older XMLHttpRequest.

๐Ÿ“Œ Syntax:

// Traditional XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET', 'url');
xhr.send();

// Modern Fetch API
fetch('url').then(response => response.json());

๐Ÿงช Example:

// Traditional AJAX with XMLHttpRequest
function makeAjaxRequest() {
    const xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && xhr.status === 200) {
            const data = JSON.parse(xhr.responseText);
            console.log('AJAX Response:', data);
        }
    };

    xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1');
    xhr.send();
}

// Modern approach with Fetch
fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(response => response.json())
    .then(data => console.log('Fetch Response:', data))
    .catch(error => console.error('Error:', error));

๐Ÿš€ Fetch API

โœ… Definition:

The Fetch API is a modern, promise-based interface for making HTTP requests in JavaScript, replacing the older XMLHttpRequest. It provides a cleaner, more flexible way to fetch resources from servers with built-in promise support. Fetch returns a promise that resolves to the Response object representing the response to the request.

๐Ÿ“Œ Syntax:

fetch(url, options)
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error(error));

๐Ÿงช Example:

// GET Request
fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(response => {
        console.log('Status:', response.status);
        return response.json();
    })
    .then(data => {
        console.log('Post Title:', data.title);
        console.log('Post Body:', data.body);
    })
    .catch(error => console.error('Error:', error));

// POST Request
fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        title: 'My New Post',
        body: 'This is the content of my post',
        userId: 1
    })
})
.then(response => response.json())
.then(data => console.log('Created Post:', data));

โš ๏ธ Error Handling in Fetch API

โœ… Definition:

Error handling in Fetch API requires checking both network errors and HTTP status codes since fetch only rejects for network failures, not HTTP error statuses. You must manually check response.ok or response.status to handle HTTP errors like 404 or 500. Proper error handling ensures your application gracefully handles both network issues and server-side errors.

๐Ÿ“Œ Syntax:

fetch(url)
    .then(response => {
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
    })
    .catch(error => console.error('Error:', error));

๐Ÿงช Example:

fetch('https://jsonplaceholder.typicode.com/posts/999') // Non-existent post
    .then(response => {
        console.log('Response status:', response.status);

        if (response.status >= 200 && response.status < 300) {
            return response.json();
        } else {
            throw new Error(`HTTP Error: ${response.status}`);
        }
    })
    .then(data => {
        console.log('Data:', data);
    })
    .catch(error => {
        console.error('Caught error:', error.message);
        // Handle error (show user message, retry, etc.)
    });

// Alternative with response.ok
fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
    })
    .then(data => console.log('Success:', data))
    .catch(error => console.error('Error:', error));

๐Ÿ“ก Axios API

โœ… Definition:

Axios is a popular JavaScript library that provides a promise-based HTTP client for making API requests with more features than the native Fetch API. It automatically handles JSON parsing, provides better error handling, supports request/response interceptors, and has built-in timeout support. Axios works in both browsers and Node.js environments with a consistent API.

๐Ÿ“Œ Syntax:

// GET request
axios.get(url).then(response => console.log(response.data));

// POST request
axios.post(url, data).then(response => console.log(response.data));

๐Ÿงช Example:

// GET Request with Axios
axios.get('https://jsonplaceholder.typicode.com/posts/1')
    .then(response => {
        console.log('Status:', response.status);
        console.log('Data:', response.data);
        console.log('Headers:', response.headers);
    })
    .catch(error => {
        console.error('Axios Error:', error.message);
        if (error.response) {
            console.error('Error Status:', error.response.status);
        }
    });

// POST Request with Axios
axios.post('https://jsonplaceholder.typicode.com/posts', {
    title: 'New Post with Axios',
    body: 'Content created using Axios library',
    userId: 1
})
.then(response => {
    console.log('Created:', response.data);
})
.catch(error => {
    console.error('Error creating post:', error);
});

// Axios with configuration
axios({
    method: 'get',
    url: 'https://jsonplaceholder.typicode.com/posts',
    timeout: 5000,
    headers: {
        'Accept': 'application/json'
    }
})
.then(response => console.log(`Found ${response.data.length} posts`));

๐ŸŽฏ Consume Promises with Async and Await

โœ… Definition:

Async/await is syntactic sugar built on top of promises that allows you to write asynchronous code that looks and behaves more like synchronous code. The 'async' keyword makes a function return a promise, while 'await' pauses the function execution until the promise resolves. This approach eliminates promise chains and makes error handling cleaner with try/catch blocks.

๐Ÿ“Œ Syntax:

async function functionName() {
    try {
        const result = await promiseFunction();
        return result;
    } catch (error) {
        console.error(error);
    }
}

๐Ÿงช Example:

// Using Promises (traditional way)
function fetchWithPromises() {
    fetch('https://jsonplaceholder.typicode.com/posts/1')
        .then(response => response.json())
        .then(data => {
            console.log('Promise way:', data.title);
            return fetch('https://jsonplaceholder.typicode.com/posts/2');
        })
        .then(response => response.json())
        .then(data => console.log('Second post:', data.title))
        .catch(error => console.error('Error:', error));
}

// Using Async/Await (modern way)
async function fetchWithAsync() {
    try {
        const response1 = await fetch('https://jsonplaceholder.typicode.com/posts/1');
        const data1 = await response1.json();
        console.log('Async way:', data1.title);

        const response2 = await fetch('https://jsonplaceholder.typicode.com/posts/2');
        const data2 = await response2.json();
        console.log('Second post:', data2.title);

    } catch (error) {
        console.error('Error:', error);
    }
}

// Call the functions
fetchWithAsync();

๐Ÿ’พ Local Storage

โœ… Definition:

Local Storage is a web storage API that allows you to store data locally within a user's browser with no expiration time. The data persists even after the browser window is closed and can store up to 5-10MB of data depending on the browser. Local Storage only accepts string values, so objects must be converted using JSON.stringify() and JSON.parse().

๐Ÿ“Œ Syntax:

// Store data
localStorage.setItem('key', 'value');

// Retrieve data
const value = localStorage.getItem('key');

// Remove data
localStorage.removeItem('key');

๐Ÿงช Example:

// Store simple data
localStorage.setItem('username', 'Vitthal');
localStorage.setItem('theme', 'dark');

// Store complex data (object)
const userSettings = {
    theme: 'dark',
    language: 'en',
    notifications: true
};
localStorage.setItem('settings', JSON.stringify(userSettings));

// Retrieve and use data
const username = localStorage.getItem('username');
console.log('Welcome back,', username); // Welcome back, Vitthal

const settings = JSON.parse(localStorage.getItem('settings'));
console.log('User theme:', settings.theme); // User theme: dark

// Check if data exists
if (localStorage.getItem('username')) {
    console.log('User is logged in');
}

// Remove specific item
localStorage.removeItem('theme');

// Clear all localStorage
// localStorage.clear();

console.log('localStorage length:', localStorage.length);

๐Ÿ”„ Session Storage

โœ… Definition:

Session Storage is similar to Local Storage but stores data only for the duration of a page session. The data is cleared when the tab is closed, making it suitable for temporary data that shouldn't persist across browser sessions. Like Local Storage, it only stores strings and has the same storage methods and capacity limitations.

๐Ÿ“Œ Syntax:

// Store data
sessionStorage.setItem('key', 'value');

// Retrieve data
const value = sessionStorage.getItem('key');

// Remove data
sessionStorage.removeItem('key');

๐Ÿงช Example:

// Store session data
sessionStorage.setItem('username', 'Vitthal');
sessionStorage.setItem('currentPage', 'dashboard');

// Store temporary user preferences
const tempSettings = {
    sortBy: 'date',
    viewMode: 'grid',
    filters: ['active', 'recent']
};
sessionStorage.setItem('tempSettings', JSON.stringify(tempSettings));

// Retrieve session data
const username = sessionStorage.getItem('username');
console.log('Current user:', username); // Current user: Vitthal

const settings = JSON.parse(sessionStorage.getItem('tempSettings'));
console.log('Current sort:', settings.sortBy); // Current sort: date

// Get first key
const firstKey = sessionStorage.key(0);
console.log('First key:', firstKey);

// Check session storage length
console.log('Session items count:', sessionStorage.length);

// Remove specific item
sessionStorage.removeItem('currentPage');

// Clear all session storage
// sessionStorage.clear();

// Session storage will be automatically cleared when tab is closed

๐Ÿ“Š Practical Example: Complete Data Fetching with Storage

๐Ÿงช Complete Example:

async function fetchAndStoreUserData() {
    try {
        // Check if data exists in localStorage
        const cachedData = localStorage.getItem('userData');

        if (cachedData) {
            console.log('Using cached data:', JSON.parse(cachedData));
            return JSON.parse(cachedData);
        }

        // Fetch new data if not cached
        console.log('Fetching fresh data...');
        const response = await fetch('https://jsonplaceholder.typicode.com/users/1');

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const userData = await response.json();

        // Store in localStorage for future use
        localStorage.setItem('userData', JSON.stringify(userData));

        // Store current session info
        sessionStorage.setItem('lastFetch', new Date().toISOString());

        console.log('Fresh data fetched and stored:', userData);
        return userData;

    } catch (error) {
        console.error('Error fetching user data:', error);

        // Try to use cached data as fallback
        const fallbackData = localStorage.getItem('userData');
        if (fallbackData) {
            console.log('Using fallback cached data');
            return JSON.parse(fallbackData);
        }

        throw error;
    }
}

// Usage
fetchAndStoreUserData()
    .then(user => {
        console.log(`Hello ${user.name}! Email: ${user.email}`);
    })
    .catch(error => {
        console.error('Failed to get user data:', error);
    });

โœ… Summary

This comprehensive guide covers the evolution of asynchronous JavaScript from basic promises to modern async/await syntax, along with practical data storage solutions. Understanding these concepts is essential for building robust, user-friendly web applications that handle asynchronous operations efficiently while providing great user experiences through proper data management and error handling.

6
Subscribe to my newsletter

Read articles from Code Subtle directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Code Subtle
Code Subtle

At Code Subtle, we empower aspiring web developers through personalized mentorship and engaging learning resources. Our community bridges the gap between theory and practice, guiding students from basics to advanced concepts. We offer expert mentorship and write interactive, user-friendly articles on all aspects of web development. Join us to learn, grow, and build your future in tech!