Object.keys(obj).length === 0 is the answer, and it is correct for the plain objects people actually mean. The catch is that it also returns true for a Date, a Map, an Error, and a class instance whose data lives in private fields — because none of those store their contents as enumerable own properties.
That is fine as long as you know what you are asking. The question people usually have is did this API return an empty object, and for a parsed JSON response the simple check is exactly right. It goes wrong when the same helper gets reused on values that are not plain objects, which happens quickly once it lives in a shared utility file.
Table of contents
- The standard check
- Guarding null and undefined
- Why Date and Map return true
- Symbols and non-enumerable properties
- The pattern you probably want in real code
- A short reference
- How this fits the rest of the stack
- FAQ
The standard check
const isEmpty = (obj) => Object.keys(obj).length === 0;
isEmpty({}); // true
isEmpty({ a: 1 }); // false
isEmpty([]); // true -- arrays have no keys either
isEmpty(new Date()); // true -- see below
Object.keys() returns an array of the object’s own enumerable string-keyed properties. Length zero means there are none, which is what empty means for a plain object.
There are two near-identical alternatives, and one is meaningfully faster:
// allocates an array just to measure it
Object.keys(obj).length === 0;
// short-circuits on the first property -- no allocation
const isEmptyFast = (obj) => {
for (const key in obj) {
if (Object.hasOwn(obj, key)) return false;
}
return true;
};
The loop version exits as soon as it finds one property, while Object.keys builds the entire array first. On an object with a thousand properties that is the difference between one iteration and a thousand plus an allocation. On the typical API response it does not matter and the readable version wins.
Object.hasOwn() is the modern replacement for hasOwnProperty and is safer, because it works on objects created with Object.create(null) that have no prototype and therefore no hasOwnProperty method.
Guarding null and undefined
Object.keys(null) throws a TypeError. So a helper that receives a value from anywhere unreliable needs a guard, and the naive one has a bug.
// buggy: returns 0 and '' rather than a boolean
const bad = (v) => v && Object.keys(v).length === 0;
// correct
const isEmptyObject = (v) =>
v != null &&
typeof v === 'object' &&
Object.keys(v).length === 0;
The buggy version relies on truthiness, so passing 0 returns 0 and passing '' returns ''. Both are falsy so the call site often behaves correctly by accident, right up until someone uses the result in a strict comparison or serialises it.
v != null with loose equality is intentional here — it is the one idiomatic use of !=, catching both null and undefined in a single check. Some linters flag it; most have an exception for exactly this comparison.
Decide explicitly what isEmpty(null) should mean for your code. There is a defensible argument for true (nothing there) and for false (not an object at all). What matters is that it is a decision rather than an accident of implementation.
Why Date and Map return true
This is the surprise that sends people to search engines, and the explanation makes it stop being surprising.
Object.keys(new Date()); // []
Object.keys(new Map([[1, 2]])); // []
Object.keys(new Set([1, 2])); // []
Object.keys(new Error('boom')); // [] in most engines
None of these store their contents as enumerable own properties. A Date holds a timestamp in an internal slot. A Map keeps its entries in internal storage reachable only through its own methods. An Error has message and stack as non-enumerable properties. Object.keys reports enumerable own properties, and there are none, so it is correct — it is answering a question about property bags, and these are not property bags.
The right checks for those types are their own:
map.size === 0;
set.size === 0;
arr.length === 0;
str.length === 0;
// plain object, excluding class instances and built-ins
const isPlainObject = (v) =>
v != null &&
typeof v === 'object' &&
(Object.getPrototypeOf(v) === Object.prototype ||
Object.getPrototypeOf(v) === null);
If your helper might receive any of these, either narrow the input type or dispatch on the type explicitly. A single isEmpty that silently claims a Date is empty is a bug waiting for a specific Tuesday.
Symbols and non-enumerable properties
Two more categories Object.keys does not see, both occasionally relevant.
const withSymbol = { [Symbol('id')]: 1 };
Object.keys(withSymbol).length; // 0
Reflect.ownKeys(withSymbol).length; // 1
const hidden = {};
Object.defineProperty(hidden, 'secret', {
value: 1,
enumerable: false,
});
Object.keys(hidden).length; // 0
Object.getOwnPropertyNames(hidden).length; // 1
Reflect.ownKeys() returns everything: string keys, symbol keys, enumerable and not. Object.getOwnPropertyNames() returns string keys including non-enumerable ones.
For the has this object got anything at all question, Reflect.ownKeys(obj).length === 0 is the most thorough answer available.
In practice this matters mostly around libraries that attach metadata via symbols specifically so it stays out of normal iteration and serialisation. If you are checking an object that has passed through an ORM or a framework, be aware there may be more there than Object.keys shows.
The pattern you probably want in real code
The empty check is frequently a proxy for a question that is better asked directly.
// instead of checking whether the response object is empty
if (isEmpty(response)) { /* ... */ }
// check the thing you actually care about
if (!response.items?.length) { /* no results */ }
if (response.error) { /* handle failure */ }
An API that returns {} for no results is usually an API design worth working around at the boundary. Normalise the response once where it enters your code, and the rest of the application never has to ask whether an object is empty.
In TypeScript, model it in the type rather than checking at runtime. Record<string, never> describes an object that can have no properties, and a discriminated union expresses success and failure far better than an emptiness test:
type Result<T> =
| { ok: true; data: T }
| { ok: false; error: string };
if (result.ok) {
// result.data is narrowed here
}
That removes the question entirely, which is the best outcome available.
A short reference
- Plain object —
Object.keys(obj).length === 0, with a null guard. - Large object in a hot path — the
for...inloop withObject.hasOwn, which short-circuits. - Array —
arr.length === 0. - Map or Set —
.size === 0. - String —
str.length === 0. - Anything at all, including symbols —
Reflect.ownKeys(obj).length === 0. - Unknown input — narrow the type first, then use the right check for it.
The theme across all of them: empty is not one concept. Deciding which type you are dealing with is the actual work, and once that is settled the check itself is one line.
How this fits the rest of the stack
Small runtime differences like whether an Error exposes enumerable properties vary between engine versions, which is one more reason a build should pin the runtime rather than inherit whatever is installed. A Node service that builds from its repository does exactly that, with the version fixed, the build log attached to the deploy that produced it, and the previous deploy available to roll back to when a dependency upgrade changes behaviour. Node services on RunxBuild covers the runtime and build configuration. When you are sizing an API alongside a managed database, the RunxBuild hosting calculator shows the service, the database, storage, and bandwidth as separate numbers.
Useful related references:
- What Does init Do in Python? It Sets Up Each Object, and self Is How
- Java Object Cache: Picking One and Not Getting Burned by It
- Python init.py: What It Actually Does, and When Empty Is Correct
- Services on RunxBuild
FAQ
How do I check if an object is empty in JavaScript?
Use Object.keys(obj).length === 0, guarded against null: v != null && typeof v === 'object' && Object.keys(v).length === 0. Object.keys(null) throws a TypeError, so the guard is not optional in shared code.
Why does Object.keys return an empty array for a Date?
A Date stores its timestamp in an internal slot rather than as an enumerable own property, so there are genuinely no keys to report. The same is true of Map, Set, and Error. Use .size for Map and Set, and narrow the type before applying a generic empty check.
What is the fastest way to check if an object is empty?
A for...in loop with Object.hasOwn that returns false on the first property found. It short-circuits, while Object.keys() builds the entire key array before measuring it. The difference only matters on large objects in hot paths.
Does Object.keys see symbol properties?
No. Object.keys returns only enumerable string-keyed own properties. Use Reflect.ownKeys(obj) to include symbol keys and non-enumerable properties, or Object.getOwnPropertyNames(obj) for string keys including non-enumerable ones.
Should I use lodash isEmpty?
It handles many types in one call, which is convenient, but it also means the emptiness question is answered by rules you have to look up. For a plain object, Object.keys(obj).length === 0 is clearer and adds no dependency. Better still, narrow the type so the question does not arise.