← Back to Articles

JavaScript: Intermediate

Introduction

Asynchronous programming is essential for JavaScript. This article covers callbacks, promises, async/await, the Fetch API, and error handling patterns.

Synchronous vs Asynchronous

// Synchronous (blocks execution)
console.log('1');
console.log('2');
console.log('3');
// Output: 1, 2, 3

// Asynchronous (non-blocking)
console.log('1');
setTimeout(() => console.log('2'), 1000);
console.log('3');
// Output: 1, 3, 2 (after 1 second)

Callbacks

// Callback function
function fetchData(callback) {
    setTimeout(() => {
        callback('Data loaded');
    }, 1000);
}

fetchData((data) => console.log(data));

// Callback hell (nested callbacks)
getUser(userId, (user) => {
    getPosts(user.id, (posts) => {
        getComments(posts[0].id, (comments) => {
            console.log(comments);
        });
    });
});

Promises

// Creating a promise
const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = true;
        if (success) resolve('Success!');
        else reject('Error!');
    }, 1000);
});

// Consuming a promise
promise
    .then(result => console.log(result))
    .catch(error => console.error(error))
    .finally(() => console.log('Done'));

// Chaining promises
fetchUser(1)
    .then(user => fetchPosts(user.id))
    .then(posts => fetchComments(posts[0].id))
    .then(comments => console.log(comments))
    .catch(error => console.error(error));

// Promise.all (wait for all)
Promise.all([promise1, promise2, promise3])
    .then(results => console.log(results));

// Promise.race (first to complete)
Promise.race([fastPromise, slowPromise])
    .then(result => console.log(result));

Async/Await

// Async function
async function fetchData() {
    return 'Data';
}

// Equivalent to
function fetchData() {
    return Promise.resolve('Data');
}

// Await (must be inside async)
async function getUserData() {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    return { user, posts };
}

// Error handling with try/catch
async function getData() {
    try {
        const response = await fetch('/api/data');
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Fetch failed:', error);
        throw error;
    }
}

// Parallel execution
async function getAllData() {
    const [users, posts, comments] = await Promise.all([
        fetchUsers(),
        fetchPosts(),
        fetchComments()
    ]);
    return { users, posts, comments };
}

Fetch API

// Basic GET request
async function fetchData() {
    const response = await fetch('/api/data');
    if (!response.ok) throw new Error('HTTP error');
    const data = await response.json();
    return data;
}

// POST request
async function createUser(user) {
    const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(user)
    });
    return response.json();
}

// With authentication
async function fetchProtectedData(token) {
    const response = await fetch('/api/protected', {
        headers: { 'Authorization': `Bearer ${token}` }
    });
    return response.json();
}

// Handle different response types
const text = await fetch('/file.txt').then(r => r.text());
const json = await fetch('/api/data').then(r => r.json());
const blob = await fetch('/image.png').then(r => r.blob());

Error Handling

// Try/catch/finally
async function loadData() {
    try {
        const data = await fetchData();
        return data;
    } catch (error) {
        console.error('Error:', error.message);
        return null;
    } finally {
        console.log('Loading complete');
    }
}

// Multiple error handling
async function robustFetch() {
    try {
        const response = await fetch('/api/data');
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        return await response.json();
    } catch (error) {
        if (error.name === 'TypeError') {
            console.log('Network error');
        } else if (error.name === 'SyntaxError') {
            console.log('Invalid JSON');
        } else {
            console.log('Unknown error');
        }
        throw error;
    }
}

Practical Example: 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
        };

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

    get(endpoint) {
        return this.request(endpoint);
    }

    post(endpoint, data) {
        return this.request(endpoint, {
            method: 'POST',
            body: JSON.stringify(data)
        });
    }

    async put(endpoint, data) {
        return this.request(endpoint, {
            method: 'PUT',
            body: JSON.stringify(data)
        });
    }

    delete(endpoint) {
        return this.request(endpoint, { method: 'DELETE' });
    }
}

// Usage
const api = new ApiClient('https://api.example.com');
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John' });

Key Takeaways

  • Use async/await for cleaner async code
  • Always handle errors with try/catch
  • Use Promise.all for parallel execution
  • Check response.ok when using Fetch
  • Use JSON.stringify for POST bodies

Next Steps