null is an empty box you put on the table on purpose, as if to say there's nothing in here and you know it. undefined is a box that's there, but nobody has put anything in it yet. Two kinds of empty let you tell "not set yet" apart from "I chose nothing".
undefined: not set yet
Make a variable and don't give it a value, and JavaScript fills in undefined for you. You also get undefined when you read a property an object doesn't have, or when a function doesn't return anything.
let toy;
console.log(toy); // prints: undefined
const cat = { name: "Mia" };
console.log(cat.age); // prints: undefinednull: empty on purpose
JavaScript never gives your variable null by itself. Someone has to write it. So when you see null, a person decided this spot should be empty.
let winner = null;
console.log(winner); // prints: null
winner = "Mia";
console.log(winner); // prints: Mia
A common use is a slot that gets filled later, like the winner of a game that hasn't ended yet. You can also set something back to null to clear it, for example when a player leaves and their seat is empty again. Anyone reading the code later can see that the empty value was chosen.
One more thing to keep apart. undefined is a value. The error ReferenceError: toy is not defined is something else. It means there's no box at all, because the variable was never created.
Comparing them
console.log(null == undefined); // prints: true
console.log(null === undefined); // prints: false
Remember double equals from the == vs === lesson? == treats null and undefined as equal. === knows they are different types and says false.
So if you want to check for exactly one of them, use ===. If you want to catch both at once, value == null does that, and it is the one place many people still use ==.
The twist: typeof null
typeof tells you what type a value is. Ask it about null and you get a wrong answer.
console.log(typeof undefined); // prints: undefined
console.log(typeof null); // prints: object
null is not an object. This is a bug from the very first version of JavaScript, in 1995. Fixing it now would break old websites, so it stays forever.
The practical lesson is to never use typeof to check for null. Compare directly with value === null.
Where the difference shows up
Data you send or save often goes through JSON, the text format used to pass data between programs. JSON knows about null, but it has no undefined. A property set to undefined simply disappears.
const pet = { name: "Mia", owner: null, age: undefined };
console.log(JSON.stringify(pet)); // prints: {"name":"Mia","owner":null}
So if a server or another program needs to know that a field is empty, send null. That's one more reason to use null when you mean empty on purpose.
Rule of thumb
Leave undefined to JavaScript, and use null when you mean empty on purpose.
Setting things to undefined yourself, like score = undefined. Later you can't tell if you cleared it or forgot to set it. Use score = null when you empty something on purpose.
Check yourself
JavaScript fills in undefined for variables that haven't been given a value. It never picks null on its own.