바쁜 분은 이것만
(reading 'x')의 x가 문제가 아니라 x 바로 앞에 있는 값이 undefined라는 뜻- 흔한 원인 4가지: 키 이름 오타·데이터 모양 착각, await 빠뜨림(아직 안 온 데이터), 배열 범위 초과·find 실패, 메서드를 꺼내 써서 this 잃음
- 에러 줄에서 x 앞 값을
console.log로 찍어 원인을 먼저 찾고, 없을 수도 있는 값만?.·??·if 확인으로 막기
TypeError: Cannot read properties of undefined (reading 'x')는 자바스크립트를 하다 보면 정말 자주 보는 에러입니다. 메시지가 길어서 겁이 나지만 읽는 법만 알면 원인을 꽤 빨리 찾을 수 있어요. Node.js v26에서 원인별로 하나씩 재현해서, 실제 메시지와 고치는 법을 같이 정리했습니다.
1. 메시지 읽는 법: 'x' 앞에 있는 값이 범인
const user = { name: "kim" };
console.log(user.address.city);
file://~/work/u1.mjs:2
console.log(user.address.city);
^
TypeError: Cannot read properties of undefined (reading 'city')
at file://~/work/u1.mjs:2:26
여기서 (reading 'city')는 "city를 읽으려고 했다"는 뜻입니다. city를 읽으려던 대상, 즉 바로 앞의 user.address가 undefined였던 거예요. user에는 address 키가 없으니까요.
- u1.mjs:2:26 → 2번째 줄 26번째 글자.
^표시가.city앞을 가리킵니다. - 점이 여러 개 이어진 줄이라면 'x' 바로 왼쪽 한 칸만 의심하면 됩니다.
user자체가 undefined였다면 메시지는(reading 'address')로 나왔을 거예요.
2. 원인 ①: 키 이름 오타, 데이터 모양 착각
API 응답이나 JSON을 다룰 때 가장 흔합니다. items를 item으로 잘못 쓴 경우예요.
const res = { data: { items: ["a", "b", "c"], total: 3 } };
console.log(res.data.item.length);
file://~/work/u2.mjs:2
console.log(res.data.item.length);
^
TypeError: Cannot read properties of undefined (reading 'length')
at file://~/work/u2.mjs:2:27
(reading 'length')이니 앞의 res.data.item이 undefined입니다. 이럴 땐 한 단계 위 값을 통째로 찍어 보면 바로 보여요.
console.log("res.data:", res.data);
console.log("keys:", Object.keys(res.data));
console.log("res.data.item:", res.data.item);
res.data: { items: [ 'a', 'b', 'c' ], total: 3 }
keys: [ 'items', 'total' ]
res.data.item: undefined
객체에 없는 키를 읽으면 에러가 아니라 조용히 undefined가 나옵니다. 그 undefined에서 한 번 더 점을 찍는 순간 에러가 나는 거라, 실제 실수는 에러 줄보다 한 칸 앞에 있어요. 키 이름의 대소문자(userId vs userID), 한 단계 더 감싸진 구조(res.items가 아니라 res.data.items)도 같은 원인입니다.
3. 원인 ②: 비동기 데이터가 아직 안 왔다 (await 빠뜨림)
서버에서 받아오는 데이터처럼 시간이 걸리는 값은 Promise로 돌아옵니다. await를 빠뜨리면 데이터가 아니라 Promise 상자를 그대로 쓰게 돼요. 아래는 0.1초 뒤에 데이터를 주는 함수로 흉내 낸 예입니다.
function loadUser() {
return new Promise((resolve) => {
setTimeout(() => resolve({ profile: { name: "kim" } }), 100);
});
}
const user = loadUser(); // await를 빠뜨림
console.log("user:", user);
console.log(user.profile.name);
user: Promise { <pending> }
file://~/work/u4.mjs:9
console.log(user.profile.name);
^
TypeError: Cannot read properties of undefined (reading 'name')
at file://~/work/u4.mjs:9:26
user를 찍어 보면 Promise { <pending> }, 아직 기다리는 중인 Promise입니다. Promise에는 profile이 없으니 user.profile이 undefined가 됐어요.
변수에 나중에 채워 넣는 방식도 똑같은 문제가 생깁니다.
let data;
setTimeout(() => {
data = { items: [1, 2, 3] };
}, 100);
console.log("data:", data);
console.log(data.items.length);
data: undefined
file://~/work/u5.mjs:7
console.log(data.items.length);
^
TypeError: Cannot read properties of undefined (reading 'items')
at file://~/work/u5.mjs:7:18
코드는 위에서 아래로 바로 실행되고, 0.1초 뒤에 올 값을 기다려 주지 않습니다. 고치는 법은 두 가지예요. 기다릴 수 있으면 await, 화면 그리기처럼 "아직 안 왔을 수도 있는" 상황이면 먼저 확인하기입니다.
const user = await loadUser();
console.log(user.profile.name);
let data;
function render() {
if (!data) {
console.log("불러오는 중...");
return;
}
console.log("개수:", data.items.length);
}
render();
data = { items: [1, 2, 3] };
render();
kim
불러오는 중...
개수: 3
(.mjs 파일이라 맨 바깥에서 바로 await를 썼어요. 일반 함수 안이라면 그 함수를 async function으로 만들어야 합니다.)
4. 원인 ③: 배열 범위 초과, find가 못 찾음
배열에서 없는 칸을 읽으면 역시 undefined가 나옵니다. 반복문 조건을 <=로 쓴 실수가 대표적이에요.
const users = [{ name: "kim" }, { name: "lee" }];
for (let i = 0; i <= users.length; i++) {
console.log(i, users[i].name);
}
0 kim
1 lee
file://~/work/u7.mjs:3
console.log(i, users[i].name);
^
TypeError: Cannot read properties of undefined (reading 'name')
at file://~/work/u7.mjs:3:27
두 번은 잘 돌고 세 번째(i = 2)에서 멈췄습니다. 길이가 2면 인덱스는 0, 1까지라 users[2]는 undefined예요. 앞쪽 출력이 정상으로 나오다가 마지막에 터지면 이 원인을 먼저 의심하면 됩니다.
for (let i = 0; i < users.length; i++) {
console.log(i, users[i].name);
}
for (const u of users) {
console.log(u.name);
}
0 kim
1 lee
kim
lee
인덱스가 꼭 필요하지 않으면 for...of가 범위 실수를 아예 막아 줍니다. 비슷하게 "없으면 undefined를 돌려주는" 것들을 한 번에 모아 봤어요.
users[2].name → TypeError: Cannot read properties of undefined (reading 'name')
users[5][0] → TypeError: Cannot read properties of undefined (reading '0')
users.find(u => u.id === 3).name → TypeError: Cannot read properties of undefined (reading 'name')
new Map().get('x').length → TypeError: Cannot read properties of undefined (reading 'length')
JSON.parse('null').name → TypeError: Cannot read properties of null (reading 'name')
(try/catch로 메시지만 모아 찍은 출력입니다.)
(reading '0')처럼 숫자가 나오면 2차원 배열이나 문자열 인덱스에서 앞 칸이 비어 있다는 뜻이에요.find()는 조건에 맞는 게 없으면 undefined,Map.get()도 없는 키면 undefined를 돌려줍니다.- 값이
null이면 메시지가of null로 바뀝니다. 원인 찾는 방법은 똑같아요.
5. 원인 ④: 메서드를 꺼내 쓰다 this를 잃음
(reading 'count')처럼 this의 속성 이름이 나오고, 에러 줄에 this.가 있다면 이 경우입니다.
class Counter {
count = 0;
increase() {
this.count++;
return this.count;
}
}
const c = new Counter();
console.log(c.increase());
const inc = c.increase; // 메서드만 꺼내면 this를 잃음
inc();
1
file://~/work/u9.mjs:4
this.count++;
^
TypeError: Cannot read properties of undefined (reading 'count')
at increase (file://~/work/u9.mjs:4:5)
at file://~/work/u9.mjs:13:1
c.increase()로 부르면 this가 c인데, 메서드만 변수에 꺼내서 inc()로 부르면 this가 undefined가 됩니다. 스택의 두 번째 줄(u9.mjs:13)이 실제로 잘못 부른 자리예요. 콜백으로 넘길 때도 똑같이 생깁니다. bind로 묶거나 화살표 함수로 감싸면 됩니다.
const inc1 = c.increase.bind(c);
const inc2 = () => c.increase();
console.log(inc1(), inc2());
1 2
6. 고치는 법: ?. 와 ?? 로 "없을 수도 있는 값" 막기
원인을 찾았는데 그 값이 원래 없을 수도 있는 값(선택 입력, 비어 있을 수 있는 응답 등)이라면 옵셔널 체이닝 ?.과 널 병합 ??를 씁니다. ?.는 앞 값이 undefined·null이면 에러 대신 undefined를 돌려주고, ??는 그때 쓸 기본값을 정해요.
const user = { name: "kim" };
const res = { data: {} };
const users = [{ name: "kim" }];
console.log(user.address?.city);
console.log(user.address?.city ?? "주소 없음");
console.log(res.data.items?.length ?? 0);
console.log(users[3]?.name ?? "(없는 사람)");
console.log(users.find((u) => u.id === 3)?.name ?? "(못 찾음)");
undefined
주소 없음
0
(없는 사람)
(못 찾음)
다만 2번처럼 키 이름 오타를 ?.로 덮으면 에러만 사라지고 값은 계속 비어 있게 됩니다. 반드시 있어야 하는 값이라면 ?.를 붙이기 전에 1~5번으로 원인부터 고치는 게 순서예요.
7. 고치는 법: if 확인, Array.isArray, 기본값 매개변수
값이 없을 때 할 일이 따로 있다면 함수 맨 앞에서 확인하고 일찍 빠져나오는 방식이 읽기 편합니다.
function printCity(user) {
if (!user || !user.address) {
console.log("주소 정보가 없습니다");
return;
}
console.log("도시:", user.address.city);
}
printCity({ name: "kim", address: { city: "Seoul" } });
printCity({ name: "lee" });
printCity(undefined);
function total(list) {
if (!Array.isArray(list)) {
console.log("배열이 아님:", list);
return 0;
}
return list.reduce((a, b) => a + b, 0);
}
console.log(total([1, 2, 3]));
console.log(total(undefined));
function greet({ name = "손님", tags = [] } = {}) {
console.log(name, tags.length);
}
greet({ name: "kim", tags: ["a"] });
greet({});
greet();
도시: Seoul
주소 정보가 없습니다
주소 정보가 없습니다
6
배열이 아님: undefined
0
kim 1
손님 0
손님 0
if (!user || !user.address): 없으면 안내만 하고 return. 아래 코드는 값이 있다고 믿고 쓸 수 있어요.Array.isArray(): 배열이 와야 하는 자리에 undefined나 객체가 올 수 있을 때.{ name = "손님", tags = [] } = {}: 인자를 아예 안 넘겨도(greet()) 빈 객체로 시작해서 에러가 나지 않습니다.
정리
| 원인 | 알아보는 법 | 고치는 법 |
|---|---|---|
| 키 이름 오타·구조 착각 | 한 단계 위 값을 console.log, Object.keys() |
키 이름·경로를 실제 데이터에 맞추기 |
| await 빠뜨림·아직 안 온 데이터 | 찍어 보면 Promise { <pending> } 또는 undefined |
await, 또는 if (!data) return |
| 배열 범위 초과·find 실패 | 몇 번 잘 돌다 마지막에 에러, (reading '0') |
i < length, for...of, find(...)?.name |
| 메서드를 꺼내 써서 this 잃음 | 에러 줄에 this. |
bind(c), () => c.method() |
| 원래 없을 수도 있는 값 | 선택 입력·빈 응답 | ?. + ?? 기본값, 기본값 매개변수 |
참고: MDN Web Docs — TypeError: "x" has no properties, Optional chaining (?.), Nullish coalescing operator (??), await, Array.prototype.find(), Array.isArray(), Default parameters, Function.prototype.bind()
