Skip to content

JavaScript Intermediate Guide

Mattscreative edited this page Feb 21, 2026 · 1 revision

JavaScript Intermediate Guide

Table of Contents


Introduction

This guide builds on the concepts from the Beginner guides. We'll cover modern JavaScript (ES6+), asynchronous programming, DOM manipulation, error handling, and functional programming concepts. By the end, you'll be ready to build real-world applications.


ES6+ Modern Features

ECMAScript 2015 (ES6) and subsequent versions introduced many features that make JavaScript more powerful and expressive.

Let and Const

We've covered these in the beginner guide, but let's dive deeper:

// let - block scoped, can be reassigned
let count = 0;
count = 1;  // OK

// const - block scoped, cannot be reassigned
const PI = 3.14159;
// PI = 3;  // Error!

// const with objects - the reference is constant, not the contents
const user = { name: "Alice" };
user.name = "Bob";  // OK - modifying property
// user = {};  // Error - reassigning

// const with arrays
const numbers = [1, 2, 3];
numbers.push(4);  // OK
// numbers = [];  // Error

// Temporal Dead Zone (TDZ)
{
    // console.log(x);  // ReferenceError!
    let x = 5;
    console.log(x);  // 5
}

Arrow Functions

Arrow functions are more than just shorter syntax - they have different behavior with this:

// Basic arrow function
const add = (a, b) => a + b;

// Single parameter - no parentheses needed
const square = x => x * x;

// No parameters
const getRandom = () => Math.random();

// Multiple statements need braces and return
const calculate = (a, b) => {
    const sum = a + b;
    const product = a * b;
    return { sum, product };
};

// Arrow functions and 'this'
function Timer() {
    this.seconds = 0;
    
    // Regular function - 'this' refers to the Timer instance
    setInterval(function() {
        this.seconds++;
        console.log(this.seconds);
    }, 1000);
    
    // Arrow function - 'this' is lexically bound
    setInterval(() => {
        this.seconds++;
        console.log(this.seconds);
    }, 1000);
}

// Arrow functions as methods (be careful!)
const person = {
    name: "Alice",
    // Arrow function here loses 'this' context
    greet: () => {
        console.log("Hello, " + this.name);  // 'this' is NOT person!
    },
    // Regular method
    introduce() {
        console.log("I am " + this.name);  // Works correctly
    }
};

Template Literals

Template literals provide powerful string formatting:

// Basic interpolation
const name = "Alice";
const greeting = `Hello, ${name}!`;
console.log(greeting);  // "Hello, Alice!"

// Expressions work too
const a = 5, b = 3;
console.log(`${a} + ${b} = ${a + b}`);  // "5 + 3 = 8"

// Multi-line strings
const poem = `Roses are red,
Violets are blue,
Sugar is sweet,
And so are you.`;
console.log(poem);

// Tagged templates
function highlight(strings, ...values) {
    return strings.reduce((result, string, i) => {
        return result + string + (values[i] ? `<mark>${values[i]}</mark>` : '');
    }, '');
}

const name = "Alice";
const result = highlight`Hello, ${name}! Welcome back.`;
console.log(result);  // "Hello, <mark>Alice</mark>! Welcome back."

Destructuring

Destructuring allows you to unpack values from arrays or properties from objects:

// Array destructuring
const colors = ["red", "green", "blue"];
const [first, second, third] = colors;
console.log(first);  // "red"

// Skip elements
const [primary, , tertiary] = colors;
console.log(primary, tertiary);  // "red", "blue"

// Rest pattern
const [head, ...tail] = colors;
console.log(head);  // "red"
console.log(tail);  // ["green", "blue"]

// Default values
const [a, b, c, d = "default"] = [1, 2, 3];
console.log(d);  // "default"

// Swap variables
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y);  // 2, 1

// Object destructuring
const person = { name: "Alice", age: 25, city: "NYC" };
const { name, age } = person;
console.log(name, age);  // "Alice", 25

// Rename variables
const { name: personName, age: personAge } = person;
console.log(personName);  // "Alice"

// Default values
const { name, country = "USA" } = person;
console.log(country);  // "USA"

// Nested destructuring
const data = {
    user: {
        profile: {
            email: "alice@example.com"
        }
    }
};
const { user: { profile: { email } } } = data;
console.log(email);  // "alice@example.com"

// In function parameters
function greet({ name, age }) {
    return `Hello, ${name}! You are ${age}.`;
}
console.log(greet({ name: "Alice", age: 25 }));  // "Hello, Alice! You are 25."

Spread and Rest Operators

The spread operator (...) expands iterables, while rest collects multiple elements:

// Spread with arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined);  // [1, 2, 3, 4, 5, 6]

// Copy array
const copy = [...arr1];
console.log(copy);  // [1, 2, 3]

// Spread with objects
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 };
console.log(merged);  // { a: 1, b: 3, c: 4 } (b is overwritten)

// Copy object
const clone = { ...obj1 };

// In function calls
const nums = [1, 2, 3, 4, 5];
console.log(Math.max(...nums));  // 5

// Rest parameters
function sum(...numbers) {
    return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4, 5));  // 15

// Destructuring with rest
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first);  // 1
console.log(rest);  // [2, 3, 4, 5]

// Object destructuring with rest
const { name, ...others } = { name: "Alice", age: 25, city: "NYC" };
console.log(name);  // "Alice"
console.log(others);  // { age: 25, city: "NYC" }

Modules (Import/Export)

Modules allow you to organize code into separate files:

// Named exports (math.js)
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;

// Default export (utils.js)
export default function formatCurrency(value) {
    return `$${value.toFixed(2)}`;
}

// Importing named exports
import { add, subtract } from './math.js';
console.log(add(2, 3));  // 5

// Import with alias
import { add as sum } from './math.js';
console.log(sum(2, 3));  // 5

// Import all as namespace
import * as MathUtils from './math.js';
console.log(MathUtils.add(2, 3));  // 5

// Import default export
import formatCurrency from './utils.js';
console.log(formatCurrency(99.9));  // "$99.90"

// Combined import
import formatCurrency, { add } from './both.js';

Asynchronous JavaScript

JavaScript is single-threaded but can handle asynchronous operations through the event loop.

Callbacks

Callbacks are functions passed as arguments to other functions:

// Synchronous callback
function processArray(arr, callback) {
    const results = [];
    for (const item of arr) {
        results.push(callback(item));
    }
    return results;
}

const numbers = [1, 2, 3];
const doubled = processArray(numbers, x => x * 2);
console.log(doubled);  // [2, 4, 6]

// Asynchronous callback (old pattern)
function fetchData(callback) {
    setTimeout(() => {
        callback(null, { name: "Alice", age: 25 });
    }, 1000);
}

fetchData((error, data) => {
    if (error) {
        console.error("Error:", error);
    } else {
        console.log("Data:", data);
    }
});

// Callback hell (avoid!)
getData(function(a) {
    getMoreData(a, function(b) {
        getEvenMoreData(b, function(c) {
            console.log(c);
        });
    });
});

Promises

Promises provide a cleaner way to handle asynchronous operations:

// Creating a promise
function fetchUser(id) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (id > 0) {
                resolve({ id, name: "Alice" });
            } else {
                reject(new Error("Invalid user ID"));
            }
        }, 1000);
    });
}

// Using promises
fetchUser(1)
    .then(user => {
        console.log("User:", user);
        return fetchUser(2);
    })
    .then(user2 => {
        console.log("User 2:", user2);
    })
    .catch(error => {
        console.error("Error:", error.message);
    })
    .finally(() => {
        console.log("Operation complete");
    });

// Promise.all - wait for all promises
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3])
    .then(values => console.log(values));  // [1, 2, 3]

// Promise.race - first one to resolve
Promise.race([
    new Promise(resolve => setTimeout(resolve, 100, "fast")),
    new Promise(resolve => setTimeout(resolve, 200, "slow"))
])
.then(value => console.log(value));  // "fast"

// Promise.allSettled - wait for all to settle
Promise.allSettled([
    Promise.resolve(1),
    Promise.reject("error"),
    Promise.resolve(3)
])
.then(results => console.log(results));
// [{status: "fulfilled", value: 1}, {status: "rejected", reason: "error"}, ...]

Async/Await

Async/await provides synchronous-looking code for asynchronous operations:

// Async function
async function getUser(id) {
    return { id, name: "Alice" };
}

// Using async/await
async function main() {
    try {
        const user = await getUser(1);
        console.log("User:", user);
    } catch (error) {
        console.error("Error:", error);
    }
}

main();

// Await with multiple promises
async function getData() {
    const [users, posts] = await Promise.all([
        fetch('/api/users').then(r => r.json()),
        fetch('/api/posts').then(r => r.json())
    ]);
    
    return { users, posts };
}

// Sequential vs parallel
async function sequential() {
    const result1 = await fetch('/api/item1');
    const result2 = await fetch('/api/item2');
    // Takes 2x as long
}

async function parallel() {
    const [result1, result2] = await Promise.all([
        fetch('/api/item1'),
        fetch('/api/item2')
    ]);
    // Takes same time as slowest request
}

Error Handling in Async Code

// Try/Catch with async/await
async function fetchData(url) {
    try {
        const response = await fetch(url);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        console.error("Fetch failed:", error);
        throw error;  // Re-throw for caller to handle
    }
}

// Handling multiple errors
async function processAll(items) {
    const results = [];
    
    for (const item of items) {
        try {
            const result = await processItem(item);
            results.push({ success: true, data: result });
        } catch (error) {
            results.push({ success: false, error: error.message });
        }
    }
    
    return results;
}

// Error boundaries pattern
async function safeAsync(fn, defaultValue) {
    try {
        return await fn();
    } catch (error) {
        console.error(error);
        return defaultValue;
    }
}

const data = await safeAsync(() => fetchData('/api/data'), []);

DOM Manipulation

The Document Object Model (DOM) represents your HTML as objects that JavaScript can manipulate.

Selecting Elements

// Single elements
const element = document.getElementById('myId');
const firstDiv = document.querySelector('div');
const elementByClass = document.querySelector('.myClass');

// Multiple elements (returns NodeList - array-like)
const allDivs = document.querySelectorAll('div');
const allItems = document.getElementsByClassName('item');

// NodeList methods
allDivs.forEach(div => console.log(div));

// Convert to real array
const arrayFromNodes = Array.from(allDivs);

// Check relationships
const parent = element.parentElement;
const children = element.children;
const nextSibling = element.nextElementSibling;
const prevSibling = element.previousElementSibling;

Modifying Elements

// Text content
element.textContent = "New text";
element.innerText = "Visible text";

// HTML content
element.innerHTML = "<strong>Bold text</strong>";

// Attributes
element.setAttribute('data-id', '123');
const id = element.getAttribute('data-id');
element.removeAttribute('data-id');

// CSS styles
element.style.color = 'blue';
element.style.backgroundColor = '#f0f0f0';
element.style.display = 'none';

// Classes
element.classList.add('active');
element.classList.remove('hidden');
element.classList.toggle('selected');
const hasClass = element.classList.contains('active');

// Dataset (data-* attributes)
element.dataset.userId = '123';
console.log(element.dataset.userId);

Event Handling

// Add event listener
element.addEventListener('click', function(event) {
    console.log('Clicked!', event.target);
});

// Arrow function
element.addEventListener('click', (event) => {
    console.log(event.clientX, event.clientY);
});

// Remove event listener (needs named function)
function handleClick(event) {
    console.log('Clicked!');
}
element.addEventListener('click', handleClick);
element.removeEventListener('click', handleClick);

// Event delegation
document.querySelector('ul').addEventListener('click', (event) => {
    if (event.target.tagName === 'LI') {
        console.log('List item clicked:', event.target.textContent);
    }
});

// Common events
element.addEventListener('click');       // Mouse click
element.addEventListener('dblclick');   // Double click
element.addEventListener('mouseenter');  // Mouse over
element.addEventListener('mouseleave');  // Mouse out
element.addEventListener('mouseover');   // Mouse enters (bubbles)
element.addEventListener('mouseout');    // Mouse leaves (bubbles)

input.addEventListener('input');         // Value changes
input.addEventListener('change');       // Value changes (on blur)
input.addEventListener('focus');        // Gets focus
input.addEventListener('blur');         // Loses focus

document.addEventListener('keydown');   // Key pressed
document.addEventListener('keyup');     // Key released

form.addEventListener('submit');        // Form submitted

Creating and Removing Elements

// Create new element
const newDiv = document.createElement('div');
newDiv.textContent = "Hello!";
newDiv.classList.add('greeting');

// Add to DOM
parent.appendChild(newDiv);  // At the end
parent.insertBefore(newDiv, parent.firstChild);  // At specific position

// Modern insertion methods
parent.append(newDiv);  // Multiple items
parent.prepend(newDiv);  // At beginning

const reference = parent.querySelector('.some-element');
reference.before(newDiv);  // Before reference
reference.after(newDiv);  // After reference

// Remove element
newDiv.remove();  // Modern method
parent.removeChild(newDiv);  // Old method

// Clone element
const clone = element.cloneNode(true);  // Deep clone (with children)
const shallowClone = element.cloneNode(false);  // Just the element

Error Handling

Proper error handling makes your code more robust and easier to debug.

Try/Catch

// Basic try/catch
try {
    const result = riskyOperation();
    console.log(result);
} catch (error) {
    console.error("Something went wrong:", error.message);
}

// Try/Catch/Finally
try {
    const data = JSON.parse(userInput);
    processData(data);
} catch (error) {
    console.error("Failed to parse:", error.message);
} finally {
    console.log("Cleanup here");  // Always runs
}

// Catching specific error types
try {
    // Code that might throw different errors
} catch (error) {
    if (error instanceof TypeError) {
        console.log("Type error:", error.message);
    } else if (error instanceof RangeError) {
        console.log("Range error:", error.message);
    } else {
        throw error;  // Re-throw unknown errors
    }
}

Throwing Errors

// Throw built-in errors
throw new Error("Something went wrong");
throw new TypeError("Expected a string");
throw new RangeError("Value out of range");
throw new SyntaxError("Invalid syntax");
throw new ReferenceError("Variable not defined");

// Custom error class
class ValidationError extends Error {
    constructor(message, field) {
        super(message);
        this.name = "ValidationError";
        this.field = field;
    }
}

function validateAge(age) {
    if (typeof age !== "number") {
        throw new TypeError("Age must be a number");
    }
    if (age < 0 || age > 150) {
        throw new ValidationError("Age must be between 0 and 150", "age");
    }
    return true;
}

Custom Errors

class AppError extends Error {
    constructor(message, code, details = {}) {
        super(message);
        this.name = this.constructor.name;
        this.code = code;
        this.details = details;
        Error.captureStackTrace(this, this.constructor);
    }
}

class NotFoundError extends AppError {
    constructor(resource) {
        super(`${resource} not found`, "NOT_FOUND", { resource });
    }
}

class UnauthorizedError extends AppError {
    constructor() {
        super("You must be logged in", "UNAUTHORIZED");
    }
}

// Usage
function getUser(id) {
    const user = database.find(id);
    if (!user) {
        throw new NotFoundError("User");
    }
    return user;
}

Functional Programming

Functional programming is a paradigm that treats computation as the evaluation of mathematical functions.

Pure Functions

// Pure function - same input always gives same output, no side effects
function add(a, b) {
    return a + b;
}

// Impure function - side effects
let total = 0;
function addToTotal(value) {
    total += value;  // Modifies external state
    return total;
}

// Impure function - different output for same input
function getRandom() {
    return Math.random();
}

// Pure version of impure functions
function addToTotalPure(total, value) {
    return total + value;
}

const newTotal = addToTotalPure(0, 5);  // Always returns 5

function getRandomPure(seed) {
    // Deterministic pseudo-random
    const x = Math.sin(seed++) * 10000;
    return x - Math.floor(x);
}

Higher-Order Functions

// Function that returns a function
function multiplier(factor) {
    return function(number) {
        return number * factor;
    };
}

const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5));  // 10
console.log(triple(5));  // 15

// Function that takes a function
function applyOperation(arr, operation) {
    return arr.map(operation);
}

console.log(applyOperation([1, 2, 3], x => x * 2));  // [2, 4, 6]

Immutability

// Mutable (avoid)
const arr = [1, 2, 3];
arr.push(4);  // Mutates original

// Immutable (preferred)
const arr = [1, 2, 3];
const newArr = [...arr, 4];  // Creates new array

// Updating object
const obj = { a: 1, b: 2 };
const newObj = { ...obj, b: 3 };  // Override b
const newObj2 = { ...obj, c: 3 };  // Add c

// Deep clone for nested objects
const deepClone = JSON.parse(JSON.stringify(original));

// Or use a library like Lodash
// import _ from 'lodash';
// const clone = _.cloneDeep(original);

// Immer library for complex immutable updates
// import { produce } from 'immer';
// const nextState = produce(state, draft => {
//     draft.users[0].name = 'Alice';
// });

Array Methods in Depth

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// map - transform each element
const doubled = numbers.map(n => n * 2);

// filter - keep elements that pass test
const evens = numbers.filter(n => n % 2 === 0);

// reduce - combine into single value
const sum = numbers.reduce((acc, n) => acc + n, 0);

// find - get first matching element
const firstEven = numbers.find(n => n % 2 === 0);

// findIndex - get index of first match
const firstEvenIndex = numbers.findIndex(n => n % 2 === 0);

// some - check if any element passes
const hasEven = numbers.some(n => n % 2 === 0);

// every - check if all elements pass
const allPositive = numbers.every(n => n > 0);

// flatMap - map then flatten
const words = ["hello", "world"];
const chars = words.flatMap(word => word.split(""));
// ["h", "e", "l", "l", "o", "w", "o", "r", "l", "d"]

// Chaining
const result = numbers
    .filter(n => n > 3)
    .map(n => n * 2)
    .reduce((a, b) => a + b, 0);

Working with APIs

Fetch API

// Basic GET request
fetch('https://api.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
    })
    .then(data => console.log(data))
    .catch(error => console.error('Error:', error));

// Async/await version
async function getData() {
    try {
        const response = await fetch('https://api.example.com/data');
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Error:', error);
    }
}

// POST request
async function createUser(userData) {
    const response = await fetch('https://api.example.com/users', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(userData)
    });
    
    if (!response.ok) {
        throw new Error('Failed to create user');
    }
    
    return response.json();
}

// Using async/await
const user = await createUser({
    name: 'Alice',
    email: 'alice@example.com'
});

JSON

// Parse JSON string to JavaScript object
const jsonString = '{"name": "Alice", "age": 25}';
const obj = JSON.parse(jsonString);

// Convert JavaScript object to JSON string
const json = JSON.stringify(obj, null, 2);  // Pretty print

// JSON with reviver function
const data = JSON.parse(jsonString, (key, value) => {
    if (key === 'age') {
        return value * 2;  // Transform value
    }
    return value;
});

// JSON with replacer
const json = JSON.stringify(obj, ['name', 'age'], 2);
// Only includes name and age properties

REST API Concepts

// RESTful API methods
const api = {
    // GET - retrieve data
    async get(url) {
        const response = await fetch(url);
        return response.json();
    },
    
    // POST - create new resource
    async post(url, data) {
        const response = await fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
        return response.json();
    },
    
    // PUT - update entire resource
    async put(url, data) {
        const response = await fetch(url, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
        return response.json();
    },
    
    // PATCH - partial update
    async patch(url, data) {
        const response = await fetch(url, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
        return response.json();
    },
    
    // DELETE - remove resource
    async delete(url) {
        await fetch(url, { method: 'DELETE' });
    }
};

// Using the API
const users = await api.get('/api/users');
const newUser = await api.post('/api/users', { name: 'Alice' });
const updatedUser = await api.patch('/api/users/1', { name: 'Bob' });
await api.delete('/api/users/1');

Practice Projects

Project 1: Todo List Application

// Simple todo list with localStorage
class TodoList {
    constructor() {
        this.todos = JSON.parse(localStorage.getItem('todos')) || [];
    }
    
    add(todo) {
        this.todos.push({
            id: Date.now(),
            text: todo,
            completed: false,
            createdAt: new Date().toISOString()
        });
        this.save();
    }
    
    toggle(id) {
        const todo = this.todos.find(t => t.id === id);
        if (todo) {
            todo.completed = !todo.completed;
            this.save();
        }
    }
    
    delete(id) {
        this.todos = this.todos.filter(t => t.id !== id);
        this.save();
    }
    
    save() {
        localStorage.setItem('todos', JSON.stringify(this.todos));
    }
}

// Usage
const todoList = new TodoList();
todoList.add("Learn JavaScript");
todoList.add("Build a project");

Project 2: Weather App Skeleton

async function getWeather(city) {
    const apiKey = 'YOUR_API_KEY';
    const url = `https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${city}`;
    
    try {
        const response = await fetch(url);
        
        if (!response.ok) {
            throw new Error('Weather data not available');
        }
        
        const data = await response.json();
        
        return {
            location: data.location.name,
            temperature: data.current.temp_c,
            condition: data.current.condition.text,
            humidity: data.current.humidity
        };
    } catch (error) {
        console.error('Error fetching weather:', error);
        return null;
    }
}

Summary

Topic Key Concepts
ES6+ Features Arrow functions, destructuring, spread, modules
Async JavaScript Promises, async/await, fetch API
DOM Manipulation Select, modify, create elements, events
Error Handling Try/catch, throwing errors, custom errors
Functional Programming Pure functions, immutability, array methods
APIs REST concepts, fetch, JSON

Next Steps


Quick Reference

// Arrow functions
const add = (a, b) => a + b;

// Destructuring
const { name, age } = person;
const [first, ...rest] = arr;

// Spread
const newArr = [...arr1, ...arr2];
const newObj = { ...obj1, ...obj2 };

// Async/await
async function fetchData() {
    try {
        const data = await fetch(url);
        return await data.json();
    } catch (error) {
        console.error(error);
    }
}

// DOM
document.querySelector('.class');
element.addEventListener('click', handler);
element.textContent = 'new text';

Clone this wiki locally