How to check if an object is empty in JavaScript
To check if an object is empty in JavaScript, Object.keys(obj).length === 0 is the answer. The JSON.stringify version reports a Date as empty.

Object.keys(obj).length === 0;
That is the answer for almost every case. It counts the object's own enumerable string keys, and zero of them means empty.
The obvious-looking alternative does not hold up:
JSON.stringify(obj) === "{}"; // works, until it does not
And the one people try first does not work at all:
obj === {}; // always false
Everything below was measured in Node v24.14.1.
Why comparing to {} never works
({}) === ({}); // false
Two object literals are two different objects. === on objects compares references, not contents, so a freshly written {} can never equal anything but itself.
The same applies to ==, to Object.is, and to any comparison that is not looking at the keys. There is no built-in "is this object empty" operator, which is why the question needs a method at all.
Where the JSON approach breaks
JSON.stringify(obj) === "{}" looks neat and has three failure modes, all measured.
A Date reports as empty.
Object.keys(new Date()).length; // 0
JSON.stringify(new Date("2026-01-01"));
// "2026-01-01T00:00:00.000Z"
A Date has no own enumerable properties, so Object.keys gives 0 and correctly calls it empty by that definition. JSON.stringify produces a quoted string rather than {}, so the JSON check calls it not empty. The two disagree, and which one is right depends on what you meant by empty.
An object holding undefined reports as empty.
JSON.stringify({ a: undefined }); // "{}"
Object.keys({ a: undefined }); // ["a"]
JSON.stringify drops keys whose value is undefined. So an object with a key is reported as empty. That is a genuine wrong answer, not a definitional disagreement, and it is the strongest reason to avoid this approach.
Key order and cost. Stringifying walks the entire object to answer a question that only needs to know whether there is at least one key. On a large object that is real work for no reason.
The definition problem
"Empty" is less obvious than it sounds, and the method you pick decides which definition you get.
const symbolOnly = { [Symbol("s")]: 1 };
Object.keys(symbolOnly).length; // 0
Reflect.ownKeys(symbolOnly).length; // 1
An object with only a symbol key is empty by Object.keys and not empty by Reflect.ownKeys.
const o = {};
Object.defineProperty(o, "hidden", { value: 1, enumerable: false });
Object.keys(o).length; // 0
An object with only non-enumerable properties is also empty by Object.keys.
In practice that is fine. Object.keys answers "does this object have any ordinary data on it", which is what the question almost always means. It is worth knowing the edges exist so the answer is a choice rather than an accident.
If you genuinely need the strictest version:
Reflect.ownKeys(obj).length === 0;
That counts string and symbol keys, enumerable or not.
The fast version
Object.keys builds an array in order to count it, which is wasted work when you only need to know whether the count is zero.
function isEmpty(obj) {
for (const key in obj) {
if (Object.hasOwn(obj, key)) return false;
}
return true;
}
This returns on the first own key it finds, so it never walks a large object. The Object.hasOwn guard is required because for...in also reports inherited properties, which is covered in looping through an object.
Worth being honest about when this matters: almost never. Object.keys(obj).length === 0 is clearer, and the difference only shows up on very large objects in a hot loop. Reach for the loop when profiling tells you to, not before.
Checking the value is an object first
Object.keys does not throw on most non-objects, which means the check can quietly succeed on input it should reject:
Object.keys("").length === 0; // true, on a string
Object.keys(5).length === 0; // true, on a number
Object.keys([]).length === 0; // true, on an array
An empty array passes an "is this object empty" check, which is correct in one sense and rarely what the caller wanted. null and undefined do throw.
A defensive version:
function isEmptyObject(value) {
return (
value !== null &&
typeof value === "object" &&
!Array.isArray(value) &&
Object.keys(value).length === 0
);
}
Three guards before the check. value !== null because typeof null is "object", and !Array.isArray because arrays are objects and almost certainly want a different test.
Related checks
Different containers need different questions, and using the wrong one is a common source of confusion:
arr.length === 0; // empty array
str.length === 0; // empty string, or str === ""
map.size === 0; // empty Map
set.size === 0; // empty Set
Object.keys(obj).length === 0; // empty object
Map and Set have a size property, which objects do not. That is another small argument for using a Map when the thing is really a lookup table: the emptiness check is one property read rather than an array construction.
Where this comes up
Three places, and the right answer differs.
An API returned no data. Often the response is {} and the check is correct. Frequently the better fix is upstream: an endpoint returning { items: [] } is easier to consume than one returning {}, because the caller does not have to guess the shape.
A form has no changed fields. Object.keys(changes).length === 0 is exactly right.
An optional config object. A default parameter is usually better than checking:
function connect(options = {}) { }
That removes the empty case rather than testing for it, which is the general pattern worth preferring where it applies.
Empty, missing and falsy are three questions
A lot of confusion here comes from conflating them.
const a = {}; // exists, empty
const b = undefined; // does not exist
const c = { x: 0 }; // exists, not empty, holds a falsy value
Each needs a different check:
Object.keys(a).length === 0; // is it empty
b == null; // is it missing (covers null and undefined)
c.x === undefined; // is a specific key absent
"x" in c; // does the key exist at all
b == null with two equals is the one deliberate use of loose equality worth keeping. It is true for exactly null and undefined and nothing else, which is usually the check you want and is shorter than writing both.
The difference between c.x === undefined and "x" in c matters when a key can legitimately hold undefined:
const d = { x: undefined };
d.x === undefined; // true
"x" in d; // true
The first says "no useful value", the second says "the key is there". Object.hasOwn(d, "x") is the version that ignores the prototype chain.
An empty object is truthy
if ({}) { } // this runs
Every object is truthy, including empty ones and empty arrays. So a truthiness check never answers "is there anything in here":
if (data) { } // only checks it is not null/undefined
if (Object.keys(data).length) { } // checks it has content
That first form is still useful as a guard before the second, since Object.keys(null) throws. The two together are the safe sequence:
if (data && Object.keys(data).length > 0) { }
Avoiding the check entirely
Often the tidiest fix is upstream. Three patterns that remove the need:
Default parameters handle a missing options object:
function connect(options = {}) { }
Optional chaining and nullish coalescing handle a missing value without a guard:
const city = user?.address?.city ?? "unknown";
A consistent API shape removes the ambiguity at the source. An endpoint that always returns { items: [] } never needs its caller to ask whether the object is empty; it asks whether the array is, which is a simpler question with no edge cases.
The general principle: checking for emptiness is usually a symptom of a value that can be several shapes. Where you control that value, making it one shape is a better fix than testing for the others.
Deep emptiness
Occasionally the question is whether an object has any useful content, not whether it has keys:
{ a: undefined, b: null } // two keys, nothing in them
{ user: { profile: {} } } // one key, empty all the way down
Object.keys(obj).length === 0 says both are non-empty, which is literally correct and often not what the caller means.
A recursive version:
function isDeeplyEmpty(value) {
if (value == null) return true;
if (typeof value !== "object") return false;
return Object.values(value).every(isDeeplyEmpty);
}
every on an empty array returns true, which is what makes {} come out empty without a special case.
Use this sparingly. It walks the whole structure, and "does this object contain anything meaningful" is usually a sign that the data model allows too many shapes. Normalising at the boundary, so absent things are absent rather than present and empty, is the better fix where you control the source.
Three common mistakes
Comparing with === {}. Always false. Objects compare by reference.
Using JSON.stringify(obj) === "{}". It reports { a: undefined } as empty, which is wrong, and disagrees with Object.keys on Dates.
Forgetting arrays pass the check. Object.keys([]).length === 0 is true. Guard with Array.isArray if an array should not count.
A reusable helper
Worth writing once rather than repeating the expression, mostly because the name states the intent:
export function isEmpty(value) {
if (value == null) return true;
if (Array.isArray(value) || typeof value === "string") return value.length === 0;
if (value instanceof Map || value instanceof Set) return value.size === 0;
if (typeof value === "object") return Object.keys(value).length === 0;
return false;
}
That treats null and undefined as empty, handles the four container types by their own measure, and reports everything else as not empty. A number is never empty, which is the sensible answer and is worth being deliberate about, since isEmpty(0) returning true would be a nasty surprise.
Whether null should count as empty is a genuine design decision rather than a fact. Treating "missing" and "present but empty" as the same thing is convenient at the call site and hides a difference that sometimes matters. The version above chooses convenience; naming it isNullOrEmpty would be more honest.
The check in TypeScript
Types do not answer this question, and it is worth knowing why:
function f(config: Record<string, string>) {
// config could still be {}
}
Record<string, string> says the values are strings when keys exist. It says nothing about whether any do. An empty object satisfies it.
So the runtime check is still required, and TypeScript's contribution is narrowing after it:
if (Object.keys(config).length > 0) {
// still Record<string, string>, TS learns nothing new
}
Where types do help is making emptiness impossible to express. A required field, a tuple with a minimum length, or a union that distinguishes the empty case all move the check to compile time:
type Result = { status: "empty" } | { status: "data"; items: Item[] };
That is the same principle as the API-shape point above, expressed in the type system: the best fix for an ambiguous value is a value that cannot be ambiguous.
Quick reference
Object.keys(obj).length === 0 // the standard check
Reflect.ownKeys(obj).length === 0 // includes symbols and non-enumerables
map.size === 0 // for a Map
arr.length === 0 // for an array
obj === {} // always false, never use
JSON.stringify(obj) === "{}" // lossy, avoid
Want to see the JSON version give a wrong answer? Start with the JavaScript track.
More from the blog

How to merge two arrays in JavaScript
To merge arrays in JavaScript, spread and concat both work. push(...arr) throws a RangeError at 200,000 items, which is why the choice matters.
Read more
How to loop through an object in JavaScript
To loop through an object in JavaScript, for...in walks inherited properties too. And integer-like keys come out first, whatever order you wrote.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.