JavaScript , from basics to async: everything I learned and how it finally clicked

JavaScript , from basics to async: everything I learned and how it finally clicked

# javascript# webdev# async# callback
JavaScript , from basics to async: everything I learned and how it finally clickedKhadija Daruwala

I went through a deep dive on JavaScript , from variables and data types all the way to closures,...

I went through a deep dive on JavaScript , from variables and data types all the way to closures, the event loop, and async/await. Here's everything I put together, written the way I wish someone had explained it to me.

I recently sat down and did a proper, structured pass through JavaScript , not just picking things up as I went, but actually understanding the mechanics behind the language. I watched a few long-form courses, took notes as I went, and then rewrote those notes in a way that made sense to me. This article is basically those notes, cleaned up and expanded into something I can share.

If you're coming from another language, or if JavaScript has always felt a bit slippery to you, I hope this helps. I'll explain things the way I actually think about them, not the way a textbook would.


Table of contents

  1. What is JavaScript?
  2. Variables , let, const, and why we don't use var anymore
  3. Data types , primitive vs non-primitive
  4. Functions , declarations, expressions, and arrows
  5. Scope , where variables live and die
  6. Closures , the concept that unlocks everything
  7. The this keyword , four rules, one order
  8. Async JavaScript , callbacks, promises, async/await
  9. The event loop , how JavaScript does many things at once

1. What is JavaScript?

JavaScript is a lightweight, dynamically-typed, interpreted language that follows the ECMAScript specification , which is basically the rulebook that defines what JavaScript is allowed to do and how it has to behave. Every browser that supports JavaScript is implementing those rules.

A few things make it distinct from languages you might have used before:

What it's good at:

  • Making web pages interactive
  • Running directly in the browser , no compilation step needed
  • Handling user events: clicks, scrolls, keypresses
  • Server-side apps via Node.js
  • Mobile and desktop apps via React Native and Electron

Key characteristics:

  • JIT compiled , runs instantly, no ahead-of-time compile step for the developer
  • Single-threaded , one thing at a time on the main thread
  • Dynamically typed , types are figured out at runtime, not declared upfront
  • Event-driven , responds to events rather than just running top to bottom

2. Variables

Variables store information , a user's name, the price of an item, a list of results. JavaScript has three ways to declare them and they behave differently.

const name = "Alice"; // Can't be reassigned. Use this by default.
let age = 25; // Can be reassigned. Use when the value changes.
var oldWay = "avoid this"; // Function-scoped, hoisted , causes bugs. Skip it.
Enter fullscreen mode Exit fullscreen mode

💡 My rule of thumb: Start with const for everything. Only switch to let when you know the value needs to change. Never use var in modern JavaScript , it has a different scoping behaviour that causes confusing bugs.


3. Data types , primitive vs non-primitive

This is one of those foundational things that trips people up when they first hit a bug. JavaScript has two categories of data types, and they behave completely differently when you copy them.

Primitive types

Primitives are the raw, basic values. The critical thing to understand: they are immutable and copied by value. When you assign a primitive to a new variable, you get a completely independent copy.

The primitive types are: String, Number, Boolean, Undefined, Null, BigInt, and Symbol.

let a = 10;
let b = a; // b gets a copy of the value 10
b = 20;
console.log(a); // Still 10 , changing b didn't touch a
Enter fullscreen mode Exit fullscreen mode

Non-primitive types

Non-primitives , objects, arrays, and functions , are mutable and copied by reference. When you assign one to a new variable, you're not copying the data. You're copying the address where that data lives in memory. Both variables now point to the same thing.

const obj1 = { name: "Alice" };
const obj2 = obj1; // obj2 points to the SAME object in memory
obj2.name = "Bob";
console.log(obj1.name); // "Bob" , they're the same object
Enter fullscreen mode Exit fullscreen mode

⚠️ Common gotcha: If you want a true independent copy of an object, you need to spread it: const obj2 = { ...obj1 };

Type coercion and equality

JavaScript will automatically convert types in certain situations , this is called type coercion, and it's responsible for some of the language's most famous quirks.

console.log(2 + "3"); // "23"  , number gets converted to a string
console.log("4" - "2"); // 2     , strings get converted to numbers
console.log("5" - true); // 4     , true coerces to 1
Enter fullscreen mode Exit fullscreen mode

This is also why the equality operator matters:

0 == ""; // true  , == allows coercion. Don't use this.
0 === ""; // false , === checks type too. Always use this.
Enter fullscreen mode Exit fullscreen mode

Simple rule: Always use ===. It checks both value and type without any coercion.


4. Functions

A function is a reusable block of code. JavaScript has three ways to write them and they all have slightly different behaviour.

// 1. Function declaration , hoisted, can be called before it's defined
function greet(name) {
  return `Hello, ${name}`;
}

// 2. Function expression , not hoisted, stored in a variable
const greet = function (name) {
  return `Hello, ${name}`;
};

// 3. Arrow function , shorter syntax, no own 'this'
const greet = (name) => `Hello, ${name}`;
Enter fullscreen mode Exit fullscreen mode

The most important practical difference: arrow functions don't have their own this. They inherit it from the surrounding scope. This makes them the right choice inside callbacks and methods where you don't want this to change unexpectedly.


5. Scope

Scope determines where a variable is accessible from. There are three kinds:

  • Global scope , variable is accessible anywhere in the program
  • Function scope , variable is only accessible inside the function it was declared in
  • Block scope , variable is only accessible inside the { } block it was declared in. This is what let and const use.
let global = "everywhere";

function example() {
  let insideFunction = "only here";

  if (true) {
    let insideBlock = "only inside this if block";
    console.log(insideBlock); // ✅ works
  }

  // console.log(insideBlock); // ❌ ReferenceError
}

// console.log(insideFunction); // ❌ ReferenceError
Enter fullscreen mode Exit fullscreen mode

6. Closures , the concept that unlocks everything

Once this clicked for me, a lot of things about JavaScript started making sense.

A closure is what happens when a function remembers the variables from the scope it was created in , even after that outer function has finished running. The inner function carries its environment with it.

function makeCounter() {
  let count = 0; // This variable lives inside makeCounter

  return function () {
    count++; // The returned function closes over 'count'
    return count;
  };
}

const counter = makeCounter();
// makeCounter() has finished running , but count is still alive

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Enter fullscreen mode Exit fullscreen mode

The count variable should have disappeared when makeCounter() finished. But because the returned function still references it, JavaScript keeps it alive. The returned function and its environment together form a closure.

🔑 Why it matters: Closures are the basis for module patterns, private variables, event handlers that remember state, and how React hooks like useState work under the hood.


7. The this keyword , four rules, one order

this is one of those things that seems confusing until you know the four rules. Once you know them and the order to check them, every this question becomes answerable.

The key insight: this is not set when you write the function. It's set at the moment the function is called. Think of it like the word "me" in English , who "me" refers to depends entirely on who is speaking.

Here are the four rules in priority order, highest to lowest:

Priority 1 - new binding (highest priority)

When you call a function with new, JavaScript creates a brand-new blank object and sets this to point at it.

function User(name) {
  this.name = name; // 'this' = the brand-new object being created
}

const alex = new User("Alex");
console.log(alex.name); // "Alex"
Enter fullscreen mode Exit fullscreen mode

Priority 2 - Explicit binding (call, apply, bind)

When you use .call(), .apply(), or .bind(), you manually tell the function what this should be.

function showName() {
  console.log(this.name);
}

const dog = { name: "Buddy" };
showName.call(dog); // "Buddy" , you forced this = dog
Enter fullscreen mode Exit fullscreen mode
  • .call() and .apply() run the function immediately
  • .bind() returns a new function with this permanently locked

Priority 3 - Implicit binding (look left of the dot)

When a function is called as a method on an object, look to the left of the dot at the moment it's called. That object is this.

const cat = {
  name: "Whiskers",
  meow() {
    console.log(this.name); // 'this' = cat
  },
};

cat.meow(); // "Whiskers" ✅

// Watch out , losing the binding
const fn = cat.meow;
fn(); // undefined ❌ , no dot, this = global
Enter fullscreen mode Exit fullscreen mode

Priority 4 - Default binding (lowest priority)

If none of the above apply, this falls back to the global object (window in browsers) or undefined in strict mode.

function standalone() {
  console.log(this); // window (browser) or undefined (strict mode)
}

standalone();
Enter fullscreen mode Exit fullscreen mode

Arrow functions - the exception to all rules

Arrow functions don't have their own this. They inherit it from wherever they were written. This is exactly why they're used inside callbacks and class methods.

const timer = {
  name: "MyTimer",
  start: function () {
    setTimeout(() => {
      // Arrow function inherits 'this' from start()
      // start() was called as timer.start() → this = timer ✅
      console.log(this.name);
    }, 1000);
  },
};

timer.start(); // "MyTimer"
Enter fullscreen mode Exit fullscreen mode

8. Async JavaScript - callbacks, promises, async/await

At its core, JavaScript is synchronous and single-threaded. But the web is full of things that take time , API calls, timers, file reads. Here's how JavaScript handles that without freezing the page.

Callbacks - the original solution

A callback is just a function you pass into another function to be called later.

setTimeout(() => {
  console.log("This runs after 2 seconds");
}, 2000);

console.log("This runs immediately");
Enter fullscreen mode Exit fullscreen mode

The problem comes when you need to chain async operations , each one depending on the previous. You end up with deeply nested callbacks. Developers called it "callback hell."

Promises - a better model

A Promise represents the eventual result of an async operation. It's always in one of three states: pending → fulfilled or pending → rejected.

fetch("https://api.example.com/users")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.log("Something went wrong:", error));
Enter fullscreen mode Exit fullscreen mode

Chaining .then() keeps code flat and readable. Always use .catch() , it catches both rejections and any exceptions thrown inside the chain.

When you need multiple promises at once:

// Wait for ALL to complete , fails fast if any one fails
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);

// Wait for all to settle, even if some fail
const results = await Promise.allSettled([req1(), req2(), req3()]);

// Return whichever resolves or rejects first
const winner = await Promise.race([slow(), fast()]);
Enter fullscreen mode Exit fullscreen mode

async/await - promises with clean syntax

async/await doesn't replace promises , it's nicer syntax on top of them. An async function always returns a promise. await pauses execution inside that function until a promise settles.

async function loadUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    console.log(user);
  } catch (error) {
    console.log("Failed to load user:", error);
  }
}
Enter fullscreen mode Exit fullscreen mode

The code reads top-to-bottom like synchronous code, but under the hood it's non-blocking and promise-based. This is the pattern you'll use in modern JavaScript.


9. The event loop , how JavaScript does many things at once

This is the one that made everything else make sense for me.

JavaScript is single-threaded , one main thread, one thing at a time. Yet it handles user clicks, API calls, and timers without freezing. That's the event loop's job.

Here's what's happening:

  • Call stack , tracks the currently executing function. Code runs line by line, functions are pushed on when called and popped off when they finish.
  • Web APIs , the browser handles background tasks like fetch() and setTimeout() outside the main thread.
  • Microtask queue , holds callbacks from completed Promises (.then(), async/await). Processed first, always.
  • Callback queue (macrotask queue) , holds callbacks from timers and DOM events. Processed after microtasks.

The rule the event loop follows:

  1. Run everything synchronous on the call stack
  2. Run all microtasks (Promises)
  3. Run one macrotask (setTimeout, etc.)
  4. Go back to step 2

This explains the most common JavaScript output question:

console.log("1. Start");

setTimeout(() => console.log("2. Timeout"), 0);

Promise.resolve().then(() => console.log("3. Promise"));

console.log("4. End");
Enter fullscreen mode Exit fullscreen mode

Output:

1. Start
4. End
3. Promise   ← microtask, runs before the timeout
2. Timeout   ← macrotask, runs last
Enter fullscreen mode Exit fullscreen mode

Even though setTimeout has a delay of 0ms, the Promise callback still runs first because microtasks always drain before macrotasks run. The 0ms means "as soon as possible after the microtask queue is empty" , not "immediately."


Wrapping up

That's everything from my deep dive into JavaScript. It took a while for some of these concepts to really click , especially closures, this binding, and the event loop , but once they did, I started being able to reason about code rather than just guessing at it.

If something here didn't land on first read, come back to it after you've written a few async functions or hit the bug it describes. Some things only make sense once you've seen the problem they solve.


Based on my notes from:


If this helped you, drop a reaction or a comment , I'm writing more of these and your feedback helps me know what to cover next.