본문으로 바로가기
TaeyoungKim.dev

JavaScript fetch 404 오류인데 catch가 실행되지 않는 이유: response.ok 확인법

웹작성 약 4분 읽기TaeyoungKim
LinkedInX

할 일을 불러왔는데 서버는 분명 404를 보냈다. 그런데 catch는 조용하고, 화면은 성공한 것처럼 다음 줄로 넘어간다. 에러가 났는데 에러 처리가 쉬는 중이라니, 처음 만나면 꽤 당황스럽다. fetch()가 실패한 것과 HTTP 요청에 오류 응답이 온 것은 서로 다른 사건이다.

fetch가 404 응답을 받아도 catch로 가지 않는 이유

fetch()가 접근 가능한 HTTP 응답을 받으면 Response 객체를 돌려준다. 그 응답의 상태가 404나 500이어도 Promise는 이 단계에서 이행된다. 따라서 아래 코드는 404 응답만으로 catch에 들어가지 않는다.

js
try {
  const response = await fetch('/api/todos/7');
  console.log(response.status, response.ok); // 404 false
} catch (error) {
  console.log('요청 자체가 실패했다', error);
}

주석의 값은 /api/todos/7이 실제로 404를 반환한다는 예시 조건이다. status는 HTTP 상태 코드이고, ok는 성공 범위인 200~299일 때 true다. 404에서는 response.ok가 false지만, 그 값만으로 예외가 자동 발생하지는 않는다.

그림처럼 HTTP 응답을 받은 경로와 응답 자체를 받지 못한 경로를 먼저 나눠 보면 catch의 침묵이 이해된다. 연결 실패나 요청 취소처럼 fetch()가 거부되는 경우는 별도로 다뤄야 한다. catch가 실행됐다고 모든 오류를 '서버 500'으로 표시하면 원인을 더 찾기 어려워진다.

response.ok로 HTTP 오류를 어떻게 처리할까?

404를 화면의 오류 흐름으로 보내려면 응답을 확인하고 직접 예외를 던지거나 오류 값을 반환해야 한다. 기존 요청을 감싸는 가장 작은 수정은 이렇다.

js
async function loadTodo(id) {
  const response = await fetch(`/api/todos/${id}`);

  if (!response.ok) {
    throw new Error(`할 일 조회 실패: HTTP ${response.status}`);
  }

  return response.json();
}

try {
  const todo = await loadTodo(7);
  console.log(todo);
} catch (error) {
  console.error(error);
}

이제 404라면 loadTodo()가 직접 던진 오류가 catch에 도착한다. 중요한 점은 fetch()가 404에서 갑자기 예외를 던지게 바뀐 것이 아니라, 우리 코드가 상태를 확인해 던졌다는 것이다. response.json()은 성공 여부 검사기가 아니다. 404 본문이 올바른 JSON이라면 파싱이 성공할 수도 있고, HTML 오류 페이지라면 JSON 파싱에서 다른 오류가 날 수도 있다.

404 응답과 네트워크 실패를 어떻게 재현할까?

다음은 외부 API 없이 로컬에서 404 JSON 응답을 만드는 작은 확인 코드다. Node.js 22에서 실행하면 fetch()는 응답을 돌려주고, if문에서 직접 던진 오류가 catch에 들어간다.

js
import { createServer } from 'node:http';

const server = createServer((_request, response) => {
  response.writeHead(404, { 'Content-Type': 'application/json' });
  response.end('{"error":"not found"}');
});

server.listen(0, '127.0.0.1', async () => {
  const url = `http://127.0.0.1:${server.address().port}/todos/7`;
  try {
    const response = await fetch(url);
    console.log(response.status, response.ok); // 404 false
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
  } catch (error) {
    console.log(error.message); // HTTP 404
  } finally {
    server.close();
  }
});

이 코드는 HTTP 오류를 수동으로 예외로 바꾸는 순간을 보여 준다. 네트워크 실패를 확인하고 싶다면 응답 코드만 바꾸지 말고, 실제로 연결할 수 없는 주소나 중단된 서버를 대상으로 별도 시험해야 한다. 그때는 상태 코드가 있는 Response 자체를 받지 못한다.

화면과 운영에서는 무엇을 구분해야 할까?

실제 화면에서는 404를 '대상을 찾을 수 없음'으로, 연결 실패를 '서버에 닿지 못함'으로 나누는 편이 좋다. loadTodo()처럼 throw로 합쳐 놓았다면 호출부의 catch 한 곳에서 두 경우를 구분할 정보도 남겨야 한다. 예를 들어 HTTP 상태를 가진 전용 오류를 쓰거나, 응답을 검사한 자리에서 사용자 메시지를 결정할 수 있다. 무조건 같은 '잠시 후 다시 시도' 문구를 보여 주면 없는 할 일을 계속 재시도하게 만든다.

또 response.ok가 참이어도 응답 본문이 항상 JSON인 것은 아니다. 본문이 없는 응답이나 형식이 다른 API라면 response.json()을 무조건 호출하지 말고 해당 API의 응답 계약에 맞춰 파싱하자. 오류를 처리하는 순서는 '응답을 받았는가 → HTTP 상태는 어떤가 → 본문 형식은 무엇인가'다.

핵심 요약

fetch()의 404는 보통 거부된 Promise가 아니라 이행된 Promise 속 오류 응답이다. response.ok 또는 status를 확인해 HTTP 오류를 직접 처리하고, 네트워크 실패·요청 취소·본문 파싱 실패와 섞지 말자. catch가 조용할 때는 먼저 서버가 보낸 상태 코드부터 확인하면 된다.

작성자

TaeyoungKim

기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

#JavaScript fetch#response.ok#HTTP 404#try catch#API 오류 처리

함께 읽으면 좋은 글