JsGuide

Learn JavaScript with practical tutorials and code examples

tutorialintermediate12 min read

Asynchronous JavaScript - Promises, Async/Await, and Fetch

Master asynchronous programming in JavaScript with promises, async/await, and the Fetch API. Learn to handle API calls, timers, and concurrent operations.

By JSGuide Team

Asynchronous JavaScript: Promises, Async/Await, and Fetch

JavaScript is single-threaded, but it can handle asynchronous operations efficiently using promises, async/await, and various APIs. This guide covers everything you need to know about asynchronous programming in JavaScript.

Understanding Asynchronous Programming #

Synchronous code executes line by line, blocking the next line until the current one completes. Asynchronous code allows other operations to continue while waiting for long-running tasks to complete.

// Synchronous (blocking)
console.log('Start');
console.log('Middle');
console.log('End');
// Output: Start, Middle, End (in order)

// Asynchronous (non-blocking)
console.log('Start');
setTimeout(() => {
    console.log('Middle');
}, 0);
console.log('End');
// Output: Start, End, Middle

The Problem with Callbacks #

Before promises, JavaScript used callbacks for asynchronous operations, which led to "callback hell":

// Callback hell - hard to read and maintain
getData(function(a) {
    getMoreData(a, function(b) {
        getEvenMoreData(b, function(c) {
            getFinalData(c, function(d) {
                // Finally got the data
                console.log(d);
            });
        });
    });
});

Introduction to Promises #

Promises provide a cleaner way to handle asynchronous operations:

// Promise syntax
const myPromise = new Promise((resolve, reject) => {
    // Asynchronous operation
    const success = true;
    
    if (success) {
        resolve('Operation successful!');
    } else {
        reject('Operation failed!');
    }
});

// Using the promise
myPromise
    .then(result => {
        console.log(result); // 'Operation successful!'
    })
    .catch(error => {
        console.log(error); // 'Operation failed!'
    });

Promise States #

A promise has three states:

  • Pending: Initial state, neither fulfilled nor rejected
  • Fulfilled: Operation completed successfully
  • Rejected: Operation failed
const pendingPromise = new Promise((resolve, reject) => {
    // Promise is pending
});

const fulfilledPromise = Promise.resolve('Success!');
const rejectedPromise = Promise.reject('Error!');

Working with Promises #

Promise.then() and Promise.catch() #

function fetchUserData(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId > 0) {
                resolve({ id: userId, name: 'John Doe', email: '[email protected]' });
            } else {
                reject('Invalid user ID');
            }
        }, 1000);
    });
}

// Using the promise
fetchUserData(1)
    .then(user => {
        console.log('User data:', user);
        return user.email; // Return value for next .then()
    })
    .then(email => {
        console.log('User email:', email);
    })
    .catch(error => {
        console.error('Error:', error);
    })
    .finally(() => {
        console.log('Operation completed');
    });

Promise Chaining #

function getUser(id) {
    return new Promise(resolve => {
        setTimeout(() => resolve({ id, name: 'Alice' }), 1000);
    });
}

function getPosts(userId) {
    return new Promise(resolve => {
        setTimeout(() => resolve(['Post 1', 'Post 2']), 1000);
    });
}

function getComments(postId) {
    return new Promise(resolve => {
        setTimeout(() => resolve(['Comment 1', 'Comment 2']), 1000);
    });
}

// Chaining promises
getUser(1)
    .then(user => {
        console.log('User:', user);
        return getPosts(user.id);
    })
    .then(posts => {
        console.log('Posts:', posts);
        return getComments(posts[0]);
    })
    .then(comments => {
        console.log('Comments:', comments);
    })
    .catch(error => {
        console.error('Error:', error);
    });

Promise.all() and Promise.allSettled() #

const promise1 = Promise.resolve(3);
const promise2 = new Promise(resolve => setTimeout(() => resolve('foo'), 1000));
const promise3 = Promise.resolve(42);

// Wait for all promises to resolve
Promise.all([promise1, promise2, promise3])
    .then(values => {
        console.log(values); // [3, 'foo', 42]
    })
    .catch(error => {
        console.error('One or more promises failed:', error);
    });

// Wait for all promises to settle (resolve or reject)
Promise.allSettled([promise1, promise2, promise3])
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`Promise ${index + 1} resolved:`, result.value);
            } else {
                console.log(`Promise ${index + 1} rejected:`, result.reason);
            }
        });
    });

Promise.race() #

const slowPromise = new Promise(resolve => setTimeout(() => resolve('slow'), 2000));
const fastPromise = new Promise(resolve => setTimeout(() => resolve('fast'), 1000));

Promise.race([slowPromise, fastPromise])
    .then(result => {
        console.log('First to complete:', result); // 'fast'
    });

Async/Await - Modern Asynchronous JavaScript #

Async/await provides a cleaner, more readable way to work with promises:

// Using promises
function fetchDataWithPromises() {
    return getUser(1)
        .then(user => {
            console.log('User:', user);
            return getPosts(user.id);
        })
        .then(posts => {
            console.log('Posts:', posts);
            return posts;
        })
        .catch(error => {
            console.error('Error:', error);
        });
}

// Using async/await
async function fetchDataWithAsyncAwait() {
    try {
        const user = await getUser(1);
        console.log('User:', user);
        
        const posts = await getPosts(user.id);
        console.log('Posts:', posts);
        
        return posts;
    } catch (error) {
        console.error('Error:', error);
    }
}

Async Function Syntax #

// Async function declaration
async function myAsyncFunction() {
    return 'Hello World';
}

// Async function expression
const myAsyncFunction = async function() {
    return 'Hello World';
};

// Async arrow function
const myAsyncArrowFunction = async () => {
    return 'Hello World';
};

// All return a Promise
myAsyncFunction().then(result => console.log(result)); // 'Hello World'

Error Handling with Async/Await #

async function fetchUserWithErrorHandling(userId) {
    try {
        const user = await fetchUserData(userId);
        const posts = await getPosts(user.id);
        const comments = await getComments(posts[0]);
        
        return { user, posts, comments };
    } catch (error) {
        console.error('Something went wrong:', error);
        throw error; // Re-throw if needed
    } finally {
        console.log('Cleanup operations');
    }
}

// Using the async function
fetchUserWithErrorHandling(1)
    .then(data => console.log('All data:', data))
    .catch(error => console.error('Final error handler:', error));

The Fetch API #

The Fetch API provides a modern way to make HTTP requests:

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

// Fetch with async/await
async function fetchPost(id) {
    try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Fetch error:', error);
        throw error;
    }
}

// Using the fetch function
fetchPost(1)
    .then(post => console.log('Post:', post))
    .catch(error => console.error('Error fetching post:', error));

Fetch Options #

// POST request with JSON data
async function createPost(postData) {
    try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer your-token-here'
            },
            body: JSON.stringify(postData)
        });
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const result = await response.json();
        return result;
    } catch (error) {
        console.error('Error creating post:', error);
        throw error;
    }
}

// Usage
createPost({
    title: 'My New Post',
    body: 'This is the content of my post',
    userId: 1
})
.then(result => console.log('Created post:', result))
.catch(error => console.error('Error:', error));

Handling Different Response Types #

async function fetchWithDifferentTypes(url) {
    try {
        const response = await fetch(url);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const contentType = response.headers.get('content-type');
        
        if (contentType && contentType.includes('application/json')) {
            return await response.json();
        } else if (contentType && contentType.includes('text/')) {
            return await response.text();
        } else {
            return await response.blob();
        }
    } catch (error) {
        console.error('Fetch error:', error);
        throw error;
    }
}

Concurrent Operations #

Running Multiple Async Operations #

// Sequential execution (slower)
async function sequentialExecution() {
    const start = Date.now();
    
    const user = await fetchUser(1);
    const posts = await fetchPosts(1);
    const comments = await fetchComments(1);
    
    const end = Date.now();
    console.log(`Sequential took ${end - start}ms`);
    
    return { user, posts, comments };
}

// Concurrent execution (faster)
async function concurrentExecution() {
    const start = Date.now();
    
    const [user, posts, comments] = await Promise.all([
        fetchUser(1),
        fetchPosts(1),
        fetchComments(1)
    ]);
    
    const end = Date.now();
    console.log(`Concurrent took ${end - start}ms`);
    
    return { user, posts, comments };
}

Handling Partial Failures #

async function fetchMultipleUsersWithFallback(userIds) {
    const results = await Promise.allSettled(
        userIds.map(id => fetchUser(id))
    );
    
    const users = [];
    const errors = [];
    
    results.forEach((result, index) => {
        if (result.status === 'fulfilled') {
            users.push(result.value);
        } else {
            errors.push({ userId: userIds[index], error: result.reason });
        }
    });
    
    return { users, errors };
}

// Usage
fetchMultipleUsersWithFallback([1, 2, 999, 4])
    .then(({ users, errors }) => {
        console.log('Successfully fetched users:', users);
        console.log('Failed to fetch users:', errors);
    });

Practical Examples #

Building a Simple API Client #

class ApiClient {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
    }
    
    async request(endpoint, options = {}) {
        const url = `${this.baseUrl}${endpoint}`;
        
        const config = {
            headers: {
                'Content-Type': 'application/json',
                ...options.headers
            },
            ...options
        };
        
        if (config.body && typeof config.body === 'object') {
            config.body = JSON.stringify(config.body);
        }
        
        try {
            const response = await fetch(url, config);
            
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            return await response.json();
        } catch (error) {
            console.error(`API request failed: ${error.message}`);
            throw error;
        }
    }
    
    async get(endpoint) {
        return this.request(endpoint, { method: 'GET' });
    }
    
    async post(endpoint, data) {
        return this.request(endpoint, {
            method: 'POST',
            body: data
        });
    }
    
    async put(endpoint, data) {
        return this.request(endpoint, {
            method: 'PUT',
            body: data
        });
    }
    
    async delete(endpoint) {
        return this.request(endpoint, { method: 'DELETE' });
    }
}

// Usage
const api = new ApiClient('https://jsonplaceholder.typicode.com');

async function demo() {
    try {
        // Get a post
        const post = await api.get('/posts/1');
        console.log('Post:', post);
        
        // Create a new post
        const newPost = await api.post('/posts', {
            title: 'My New Post',
            body: 'This is the content',
            userId: 1
        });
        console.log('Created post:', newPost);
        
        // Update a post
        const updatedPost = await api.put('/posts/1', {
            title: 'Updated Title',
            body: 'Updated content',
            userId: 1
        });
        console.log('Updated post:', updatedPost);
        
    } catch (error) {
        console.error('Demo error:', error);
    }
}

demo();

Implementing Retry Logic #

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
    for (let i = 0; i <= maxRetries; i++) {
        try {
            const response = await fetch(url, options);
            
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            return await response.json();
        } catch (error) {
            if (i === maxRetries) {
                throw error;
            }
            
            console.log(`Attempt ${i + 1} failed, retrying...`);
            
            // Wait before retrying (exponential backoff)
            const delay = Math.pow(2, i) * 1000;
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

Best Practices #

1. Always Handle Errors #

// Bad
async function badExample() {
    const data = await fetch('/api/data');
    return data.json();
}

// Good
async function goodExample() {
    try {
        const response = await fetch('/api/data');
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        return await response.json();
    } catch (error) {
        console.error('Error fetching data:', error);
        throw error;
    }
}

2. Use Promise.all() for Concurrent Operations #

// Bad (sequential)
async function badConcurrency() {
    const user = await fetchUser(1);
    const posts = await fetchPosts(1);
    const comments = await fetchComments(1);
    return { user, posts, comments };
}

// Good (concurrent)
async function goodConcurrency() {
    const [user, posts, comments] = await Promise.all([
        fetchUser(1),
        fetchPosts(1),
        fetchComments(1)
    ]);
    return { user, posts, comments };
}

3. Avoid Async/Await in Loops #

// Bad
async function badLoop(items) {
    const results = [];
    for (const item of items) {
        const result = await processItem(item);
        results.push(result);
    }
    return results;
}

// Good
async function goodLoop(items) {
    const results = await Promise.all(
        items.map(item => processItem(item))
    );
    return results;
}

Common Pitfalls and Solutions #

1. Not Handling Promise Rejections #

// This will cause an unhandled promise rejection
fetch('/api/data').then(response => response.json());

// Always add .catch() or use try/catch
fetch('/api/data')
    .then(response => response.json())
    .catch(error => console.error('Error:', error));

2. Mixing Promises and Async/Await #

// Inconsistent (mixing styles)
async function mixedStyle() {
    const user = await fetchUser(1);
    return fetchPosts(user.id)
        .then(posts => posts.filter(post => post.published))
        .catch(error => console.error(error));
}

// Consistent (use async/await throughout)
async function consistentStyle() {
    try {
        const user = await fetchUser(1);
        const posts = await fetchPosts(user.id);
        return posts.filter(post => post.published);
    } catch (error) {
        console.error(error);
    }
}

Summary #

Asynchronous JavaScript is essential for modern web development. Key concepts include:

  • Promises provide a clean way to handle asynchronous operations
  • Async/await makes asynchronous code look and behave more like synchronous code
  • Fetch API is the modern way to make HTTP requests
  • Promise.all() enables concurrent execution of multiple async operations
  • Error handling is crucial for robust asynchronous code

Practice these concepts to build responsive, efficient JavaScript applications!

Next Steps #

Related Tutorials

TutorialBeginner
4 min min read

Best Way to Learn JavaScript: Common Questions Answered

Find answers to frequently asked questions about the best way to learn JavaScript, including timelines, resources, and effective study methods.

#javascript #learning #faq +2 more
Read Tutorial →

Last updated: Jan 11, 2025

Tutorialintermediate

DOM Manipulation with JavaScript - Complete Guide

Master DOM manipulation in JavaScript. Learn to select elements, modify content, handle events, and create dynamic web pages with practical examples.

#javascript #dom #manipulation +2 more
Read Tutorial →
Tutorialintermediate

ES6+ Features Every JavaScript Developer Should Know

Master modern JavaScript with ES6+ features including arrow functions, destructuring, modules, async/await, and more essential syntax improvements.

#javascript #es6 #modern +2 more
Read Tutorial →
TutorialBeginner
5 min min read

How to Turn On JavaScript: Complete Guide for All Browsers

Learn how to turn on JavaScript in Chrome, Firefox, Safari, and Edge with step-by-step instructions and troubleshooting tips.

#javascript #enable #browser +2 more
Read Tutorial →

Last updated: Jul 11, 2025