< BACK
DEV.CARDS

JAVASCRIPT

00000001VARIABLES & TYPES
BASICSTYPES
let   x = 10;          // reassignable
const y = 20;          // constant — prefer this
// var z;               ← avoid! (function-scoped)

let name  = 'Alice';    // string
let age   = 25;         // number (int+float same type)
let ok    = true;       // boolean
let empty = null;       // intentionally empty
let undef;              // undefined (never assigned)
let big   = 9007n;      // BigInt
let sym   = Symbol('id'); // unique

typeof 'hi'    // 'string'
typeof 42     // 'number'
typeof null   // 'object'  ← historic JS bug!
VARIABLES & TYPES
what is a variable?
📦 A variable is a labelled box. You put a value inside and give the box a name so you can find it later. let age = 25 means "create a box called age and put 25 inside it." Anywhere you write age later in the code, JS opens that box and uses what's inside.
const vs let vs var
Use const by default — it seals the box. Anyone reading your code knows the value won't be replaced.

Use let only when you genuinely need to reassign — loop counters, accumulating totals, state toggles.

⚠ Never use var. It was the original keyword before 2015. It ignores block scope — a variable declared inside an if with var leaks outside the curly braces, causing subtle bugs. Just forget it exists.
the 7 primitive types
  • string — text in quotes. Backtick strings let you embed expressions: `Hello ${name}`
  • number — any number. JS doesn't separate integers from decimals — both are just number
  • boolean — only true or false. Like a light switch
  • null — intentionally empty. "This box is empty on purpose"
  • undefined — JS assigned this automatically; you never gave it a value
  • Symbol — guaranteed-unique identifier. Rare in everyday code
  • BigInt — numbers too large for regular number. Add n suffix: 9007n
⚠ typeof null === 'object' — This is a famous bug from 1995 that was never fixed because it would break millions of existing websites. Just memorise this quirk.
00000010FUNCTIONS
BASICSARROWREST
function greet(name) {
  return `Hello ${name}`;
}

// Arrow — shorter, inherits outer `this`
const add    = (a, b) => a + b;
const square = n => n * n;
const hi     = () => 'hi';

// Default parameter
const welcome = (name = 'World') =>
  `Welcome, ${name}`;

// Rest — collects remaining args into array
const sum = (...nums) =>
  nums.reduce((a, b) => a + b, 0);
sum(1, 2, 3); // → 6
FUNCTIONS
what is a function?
🏭 A function is a reusable recipe. You write the steps once, name them, then "call" (run) the recipe as many times as you want with different ingredients (inputs). greet("Alice") runs the greet recipe with Alice, handing back "Hello Alice!"
The values you pass when calling are arguments. The names in the definition are parameters. return is the output — what the function hands back.
declaration vs arrow
Declarations are hoisted — JS moves them to the top before running, so you can call them before you wrote them. Occasionally handy.

Arrow functions are the modern choice. Two key differences:
  • Shorter — great for one-liners
  • No own this — they inherit it from the enclosing scope, making them safe in class callbacks
💡 Use arrow functions for almost everything. Use declarations for top-level named functions only.
template literals
Backtick strings with ${} are template literals. The ${} slot accepts any JavaScript expression — variable, math, ternary, function call. Vastly cleaner than string concatenation: `Hello ${name}!` vs "Hello " + name + "!".
default & rest parameters
Default: name = "World" provides a fallback when the caller passes nothing (or passes undefined).

Rest (...nums): collects all remaining arguments into a real array. Useful when you don't know in advance how many values someone will pass.
00000011ARRAYS
MAPFILTERREDUCE
const arr = [1, 2, 3, 4];

// Mutating (changes original):
arr.push(5)         // add end → [1,2,3,4,5]
arr.pop()          // remove last
arr.unshift(0)      // add front
arr.splice(1,1)    // remove 1 item at index 1
arr.sort((a,b)=>a-b) // numeric sort — always pass fn!

// Non-mutating (returns new array):
arr.map(x => x * 2)      // [2,4,6,8]
arr.filter(x => x > 2)    // [3,4]
arr.slice(1, 3)          // [2,3]

// Returns a single value:
arr.reduce((acc,x)=>acc+x, 0) // 10
arr.find(x => x > 2)          // 3
arr.includes(2)               // true
arr.some(x => x > 3)          // true
arr.every(x => x > 0)          // true
ARRAYS
what is an array?
📋 An array is an ordered, numbered list. Index 0 = item #1, index 1 = item #2. arr[0] = first item. Arrays can hold anything — numbers, strings, objects, even other arrays — all mixed together.
the big three: map, filter, reduce
map() — Transform every item and get a new list. Like saying "for each item, apply this recipe." The original is untouched, the result has the same length.

filter() — Keep only items where your test function returns true. Like crossing out things that don't qualify. Result may be shorter.

reduce() — Collapse all items into one value. Add up numbers, build an object from an array. The second argument is the starting value (use 0 for sums).

💡 All three never mutate the original — they return new arrays. Chain them: arr.filter(x => x > 0).map(x => x * 2)
the sort() gotcha
⚠ sort() without a comparator is alphabetical — even for numbers! [10, 2, 30].sort() gives [10, 2, 30] because "1" comes before "2" alphabetically. Always pass (a, b) => a - b for ascending numeric sort. Forgetting this is a rite of passage.
quick reference
  • find(fn) — first matching item (or undefined)
  • findIndex(fn) — index of first match (or -1)
  • includes(x) — does x exist? → boolean
  • some(fn) — at least one match?
  • every(fn) — all items match?
  • flat() — flatten one level of nesting
  • slice(1, 3) — copy from index 1 up to (not including) 3
00000100OBJECTS & DESTRUCTURING
OBJECTSSPREADDESTRUCT
const user = { name: 'Alice', age: 25 };

user.name;        // dot access
user['age'];     // bracket (dynamic key)

// Destructuring — unpack multiple at once
const { name, age } = user;
const { name: n, age = 18 } = user;
//              ↑ rename   ↑ default

// Spread — copy + override (immutable update)
const updated = { ...user, age: 26 };

// Shorthand — when key = variable name
const city = 'Jakarta';
const obj = { name, city }; // same as name:name

Object.keys(user)    // ['name','age']
Object.values(user)  // ['Alice',25]
Object.entries(user) // [['name','Alice'],['age',25]]
OBJECTS & DESTRUCTURING
what is an object?
🗂️ An object is a form with labelled fields. An ID card has fields: name, age, role. An object is exactly that — a collection of named values. Each name is a "key," its content is a "value." While arrays use numbers as index (arr[0]), objects use string names (user.name).
Use dot notation when the key name is known ahead of time. Use bracket notation user["age"] when the key is stored in a variable.
destructuring — the time-saver
Without destructuring:
const name = user.name;
const age = user.age;

With destructuring (one line):
const { name, age } = user;

Rename: { name: n } = "pull out name, call it n locally."
Default: { age = 18 } = "use 18 if the field doesn't exist or is undefined."

Works on arrays too: const [a, b, ...rest] = [1, 2, 3, 4];
spread — the copy machine
{ ...user, age: 26 } = "copy everything from user, then override age with 26." Creates a brand new object — the original is untouched.

This pattern is everywhere in React for immutable state updates: { ...oldState, loading: false }

⚠ Spread is shallow! Nested objects are copied by reference, not cloned. If user.address is an object, both the copy and original share the same address object. Change one, you change both.
Object utilities
Object.keys(), Object.values(), and Object.entries() let you loop over objects. They convert an object's parts into arrays, which you can then chain with .map(), .filter(), etc. entries() is especially powerful — it gives you [key, value] pairs perfect for rebuilding a modified object.
00000101ASYNC / AWAIT & PROMISES
ASYNCPROMISESFETCH
async function getUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(res.status);
    return await res.json();
  } catch (err) {
    console.error('Failed:', err);
  }
}

// Parallel requests — much faster!
const [users, posts] = await Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
]);

// allSettled — gets results even if some fail
const results = await Promise.allSettled([p1, p2]);

// Custom delay
const delay = ms => new Promise(
  res => setTimeout(res, ms));
await delay(1000); // pause 1s
ASYNC / AWAIT & PROMISES
why does async/await exist?
☕ JavaScript is like a single-threaded barista. When you order coffee (a network request), the barista doesn't freeze staring at the machine. They take your order, go serve others, and come back when the coffee is ready. That "come back when ready" mechanic is what Promises handle. async/await is syntactic sugar that makes this look like normal sequential code.
Without async/await, you'd write messy .then().then().catch() chains ("callback hell"). async/await reads top-to-bottom like synchronous code — just with await before anything that takes time.
step-by-step breakdown
1. Mark the function async. Now it can use await.
2. Put await before any Promise (like fetch()). This pauses that function until the Promise resolves — JS keeps running other things meanwhile.
3. Wrap in try/catch. Any thrown error (or rejected Promise) is caught here.

⚠ fetch() does NOT throw on HTTP errors! A 404 or 500 response still "succeeds" as far as fetch is concerned — it just sets res.ok = false. You MUST manually check if (!res.ok) throw new Error(...). Every beginner gets burned by this at least once.
sequential vs parallel
Two awaits in a row run one after another — each waits for the previous to finish. If each takes 500ms, total is 1000ms.

Promise.all([p1, p2]) fires both simultaneously and waits for whichever is slower. Total: ~500ms. Always use Promise.all when requests are independent of each other.

⚠ If ONE promise in Promise.all fails, the whole thing rejects. Use Promise.allSettled when you want results from all of them regardless of failures — it gives you {status, value|reason} for each.
00000110CONTROL FLOW
LOGICLOOPSOPERATORS
// Ternary — inline if/else
const label = age >= 18 ? 'adult' : 'minor';

// ?? — fallback for null/undefined ONLY
const val = input ?? 'default';
// 0, '', false still pass through!

// ?. — safe property access chain
const city = user?.address?.city;
const r    = fn?.();   // call only if fn exists

// Loops
for (const item of arr)  {} // values
for (const key  in obj)  {} // keys
arr.forEach((v, i) => {}) // can't break early

// Short-circuit
isAdmin && showPanel(); // right runs only if left truthy
val || 'fallback';     // right if left is ANY falsy
val ?? 'fallback';     // right only if null/undefined
CONTROL FLOW
ternary — compact if/else
condition ? valueIfTrue : valueIfFalse

Read it as "if age is 18+, give me 'adult', otherwise 'minor'." Use ternary for simple one-liners. For complex multi-branch logic, regular if/else is more readable.
⚠ Never nest ternaries inside ternaries. It becomes unreadable instantly.
?? vs || — a crucial distinction
Both provide a fallback value, but they differ on what triggers it:

|| triggers for any falsy: null, undefined, 0, "", false, NaN
?? triggers only for null or undefined

⚠ Real bug: A user sets their score to 0. With score || 100, you display 100 instead of 0 because 0 is falsy. With score ?? 100, you correctly show 0. When in doubt, ?? is almost always what you actually mean.
optional chaining ?.
Normally, user.address.city throws a TypeError if address is null. The whole script crashes.

user?.address?.city means: "try to access address — if it's null/undefined, give me undefined instead of crashing." Each ?. is a safety guard. Essential when working with API responses where fields might be missing.
loops — which to use
  • for...of → gives you values. Use for arrays.
  • for...in → gives you keys (property names). Use for objects.
  • forEach → like for...of but as a method. Cannot break or return early.
  • Classic for (let i=0; i<n; i++) → when you need the index number.
00000111CLASSES, CLOSURES & MODULES
OOPCLOSURESMODULES
class Animal {
  #sound = '...';    // private field
  static count = 0;   // class-level
  constructor(name) {
    this.name = name;
    Animal.count++;
  }
  speak() { return this.#sound; }
}
class Dog extends Animal {
  speak() { return 'Woof!'; }
}

// Closure — function remembers its birthplace
function makeCounter() {
  let n = 0;
  return () => ++n; // closes over n
}
const c = makeCounter(); c(); // 1  c(); // 2

// ES Modules
export const PI = 3.14;           // named
export default function main() {}    // default
import main, { PI } from './mod';
CLASSES, CLOSURES & MODULES
classes — blueprints for objects
🏗️ A class is a cookie cutter. new Dog("Rex") uses the Dog cutter to stamp out a specific Dog instance. The constructor is the setup instructions that run when stamping.
#sound (private): the # prefix means this property can only be accessed inside the class. Complete encapsulation.
static count: belongs to the class itself, not any instance. All instances share one copy.
extends: Dog inherits everything from Animal automatically and can override specific methods.
closures — a superpower
A closure is a function that remembers the variables from where it was created — even after the outer function has returned and is "done."

In makeCounter: the inner function closes over n. Even after makeCounter returns, calling c() still increments that same n. The variable lives on inside the closure.

💡 Closures are everywhere: React useState, event handlers, setTimeout callbacks, factory functions that remember configuration. Understanding closures is what separates beginners from intermediate developers.
ES modules — splitting code into files
  • Named export: export const PI = 3.14 — many per file. Import with exact name in {}
  • Default export: export default function main() — one per file. Import with any name you choose
  • Import all: import * as utils from "./utils" — gives you an object with all named exports
00001000ERROR HANDLING
ERRORSTRY/CATCH
try {
  const d = JSON.parse(badStr);
} catch (err) {
  if (err instanceof SyntaxError) {
    console.error('Bad JSON');
  } else throw err; // re-throw unknown!
} finally {
  // always runs — cleanup here
}

// Custom error class
class AppError extends Error {
  constructor(msg, statusCode = 500) {
    super(msg);
    this.statusCode = statusCode;
    this.name = 'AppError';
  }
}
throw new AppError('Not found', 404);

// Safe JSON parse helper
const safeJSON = (str, fallback = null) => {
  try { return JSON.parse(str); }
  catch { return fallback; }
};
ERROR HANDLING
what happens without error handling?
💥 A chef with no plan B: if an ingredient is missing, the whole kitchen stops. That's an unhandled error in JS — the entire script crashes. try/catch gives the kitchen a plan B.
try / catch / finally breakdown
try { } — put risky code here. Any code that might throw.

catch (err) { } — runs only if something in try throws. The err object has:
  • err.message — human-readable description
  • err.name — the error type: "TypeError", "SyntaxError", etc.
  • err.stack — full stack trace showing where it came from
finally { } — runs regardless of success or failure. Use it for cleanup: close DB connections, hide loading spinners, release file handles.

⚠ Always re-throw errors you don't recognise. Silently swallowing unknown errors hides real bugs. Only handle what you understand; re-throw the rest.
custom error classes
Plain Error only has a message. By extending it, you attach extra metadata — like an HTTP status code. Now your error handler can check err.statusCode and send the right HTTP response automatically.

Always set this.name in custom error classes so instanceof checks work correctly after bundling.