문제 정의
에러 원문: How to catch exception correctly from http.request()? 출처: StackOverflow 35326689
Angular 2의 Http 서비스를 이용해 http.request()를 호출하고, 응답을 처리하는 체인에 .catch() 연산자를 추가하려고 했지만 .catch가 함수가 아니라는 TypeError가 발생하는 상황이다. 댓글에서는 추가로 Observable.throw가 함수가 아니다는 오류도 확인되었다.
즉 map은 정상 동작하는데 catch만 실패한다. 이 때문에 .catch(this.handleError) 한 줄만 주석 처리하면 코드가 정상 동작한다.
에러 로그 / 문제 코드
| |
| 발생 오류 | 문맥 |
|---|---|
TypeError: ... .catch is not a function | .catch() 연산자를 사용한 라인 |
EXCEPTION: TypeError: Observable_1.Observable.throw is not a function | handleError 안에서 Observable.throw() 호출 |
map이 잘 동작하는데도 catch만 안 되는 점이 이 문제의 특징이다.
원인 탐구
map은 잘 동작하는데 catch만 실패한다는 사실이 가장 큰 단서다. 코드를 보면:
| |
위처럼 map 연산자만 명시적으로 임포트되어 있다. 반면 catch는 어디에도 임포트되지 않았다.
이 현상은 RxJS 5의 “패치(patch) 방식 연산자” 설계에서 비롯된다.
원인 1 — RxJS 5는 연산자를 자동으로 불러오지 않는다
RxJS 5([email protected])에서는 기본 Observable 임포트만으로 모든 연산자를 사용할 수 없다. 각 연산자는 몽키패치(prototype 확장) 방식으로 Observable에 추가되며, 사용자가 원하는 연산자만 경로로 불러와야 한다.
Observable.prototype에 map은 추가됐지만 catch는 추가되지 않았기 때문에, 런타임에 .catch()를 호출하면 Observable 프로토타입에 해당 메서드가 존재하지 않아 catch is not a function이 발생한다.
원인 2 — 팩토리 함수도 마찬가지
Observable.throw()는 연산자가 아니라 Observable의 정적 팩토리(static factory) 메서드다. 이것도 마찬가지로 별도 임포트가 필요하다. 임포트하지 않으면 Observable.throw is not a function이 발생한다.
(참고: 이후 RxJS 5.5+ 에서는 pipeable operator 방식으로, RxJS 6+ 에서는 catchError, throwError라는 이름으로 바뀌고 패치 임포트가 사라졌다. 이 글은 질문 시점인 RxJS 5 기반 해결책이다.)
근본 원인 분석
근본 원인은 RxJS 5의 연산자 로딩 방식이다. 요약하면:
- Angular 2(아직
[email protected]였던 시기)에서import { Observable } from 'rxjs/Observable'는 Observable 클래스 그 자체만 가져온다. map,catch같은 연산자는 항상 기본 포함되지 않는다.- → 표현식이 길고 흔히 쓰는 연산자(
map)는 꼭 임포트해야 쓸 수 있고, - → 같은 이유로
catch도 임포트하지 않으면Observable.prototype에 존재하지 않는다.
- → 표현식이 길고 흔히 쓰는 연산자(
Observable.throw같은 정적 팩토리도 마찬가지로 별도 임포트가 필요하다.
즉, “러브스 map만 되고 catch는 왜 안 되지?“라는 혼란이 생기지만, 사실 둘 다 원칙적으로 같은 규칙을 따른다. map을 임포트했을 뿐 catch와 throw를 임포트하지 않은 것이 전부다.
| 필요한 심볼 | 타입 | 임포트 경로 |
|---|---|---|
Observable (기본) | 클래스 | import { Observable } from 'rxjs/Observable' |
map | 연산자 (prototype) | import 'rxjs/add/operator/map' |
catch | 연산자 (prototype) | import 'rxjs/add/operator/catch' |
throw | 정적 팩토리 | import 'rxjs/add/observable/throw' |
코드 해결책
해결책 1 — 누락된 연산자/팩토리 임포트 추가 (채택된 답변)
누락된 임포트 두 줄을 추가하면 해결된다:
| |
핵심 변경:
| |
이 두 줄만 추가하면 catch is not a function과 Observable.throw is not a function 모두 해결된다.
해결책 2 — catch 대신 subscribe의 에러 콜백 사용
예외를 꼭 연산자로 처리할 필요가 없다면, subscribe의 에러 콜백으로도 처리할 수 있다:
| |
이 방식은 별도 연산자 임포트가 필요 없다는 장점이 있다. 다만 결과가 Subscription이 되어 외부에서 map된 값으로 계속 다루기 어려워진다.
어느 쪽을 선택할까?
| 상황 | 권장 |
|---|---|
| 반환값을 옵저버블 체인으로 계속 이어갈 때 | 해결책 1 (연산자 임포트 + .catch()) |
| 최종 소비만 하고 로그/간단 처리가 목적일 때 | 해결책 2 (subscribe 에러 콜백) |
두 오류(catch is not a function, Observable.throw is not a function)를 확실히 없애려면 해결책 1의 임포트 추가가 가장 깔끔하고, 채택 답변도 이 방법을 제시한다.
향후 예방 조치
RxJS 5 연산자는 “쓸 때마다 임포트"가 원칙이다.
map은 물론catch,finally,retry등 모든 연산자를 사용하기 전에import 'rxjs/add/operator/<이름>'을 확인한다.- 정적 팩토리(
Observable.of,Observable.throw,Observable.from등)도import 'rxjs/add/observable/<이름>'이 필요하다.
오류 메시지가 곧 누락된 임포트를 가리킨다.
... is not a function형태의 TypeError는 대부분 해당 메서드/팩토리가 Observable에 등록되지 않았음을 뜻한다. 경로를 찾아 임포트하면 대부분 해결된다.
버전을 확인하자.
- RxJS 5:
import 'rxjs/add/...'(패치 연산자/팩토리) — 이 글의 방식. - RxJS 5.5+:
import { catchError } from 'rxjs/operators'와pipe()(pipeable operator)를 권장. - RxJS 6+: 연산자가
catchError, 에러 생성이throwError로 이름이 바뀌었고,Object.import가 아닌 명명된 임포트만 사용한다. 프로젝트의package.json의rxjs버전을 먼저 확인하고 그 버전에 맞는 문법을 사용하는 것이 혼란을 줄인다.
- RxJS 5:
관련 심볼은 함께 임포트한다.
.catch()와 그 안에서 쓰는Observable.throw는 함께 쓸 가능성이 높으므로 두 임포트를 한 번에 같이 추가해 두 번째 오류가 뜨는 일을 막는다.
이렇게 하면 “한 라인만 주석 처리하면 잘 돌아가는” 상태를 예방하고, 예외를 옵저버블 체인 안에서 안정적으로 처리할 수 있다.
출처
- StackOverflow — How to catch exception correctly from http.request()? (질문 id 35326689, 조회수 332,793 / 점수 161)