throwError is not throw error
The American poet Edward Estlin Cummings was famous for his eccentric use of spacing and capitalization, to the point that his name is usually styled as e e cummings. I wonder what he would think of an RxJS question that a friend asked me: “Is returning throwError the same as writing ‘throw error’?”

The American poet Edward Estlin Cummings (1894–1962) was famous for his eccentric use of spacing and capitalization, to the point that his name is usually styled as e e cummings. I wonder what he would think of an RxJS question that a friend asked me: “Is returning throwError
the same as writing ‘throw error
’?”
The short answer (whatever e e would say) is no. They are completely different, but the fact that my friend asked is interesting.
throwError()
is a function; specifically, it’s a function that returns an Observable that immediately errs out.
Let’s say you are writing a function that takes an ID and looks up that ID in a remote database, something like this:
function getItem(id: string): Observable<Item> {
return http.get(`https://database.xyz/items?id=${id}`);
}
After a while, you notice that many times, the function is being called with an empty ID. The backend eventually rejects the request, but you have to wait for that happen. There are a lot of things you could do, but the best is probably
With this change, the interface to the function remains unchanged: it always returns an Observable, and the request fails for whatever reason, the Observable errs out. All that changes is that with an invalid ID, it errs out instantly without burdening the server. A use like this is the intended purpose of throwError.
Throwing an error is, of course, completely different. It’s not even a function; it’s a statement that interrupts the flow of control. If you had re-written getItem()
by having it throw an error, like this:
function getItem(id: string): Observable<Item> {
if (!id) {
throw new Error("invalid ID");
}
return http.get(`https://database.xyz/items?id=${id}`);
}
That looks almost the same, but it’s just a terrible idea. It actually changes the API: you (or somebody) needs to find every place that calls the function to catch and possibly refactor the code to handle the new exception — and of course, you also still need all the old error-handling code for cases where the network call is tried, but fails.
Why was my friend curious about whether these two very different things were the same? He had seen several instances where the two mechanisms had been used interchangeably. Imagine that instead of a getItem()
function, you needed to change a stream of IDs into a stream of Items. This way of writing the code:
const items = ids.pipe(
concatMap(id => {
if (!id) {
return throwError("invalid ID");
}
return http.get(`https://database.xyz/items?id=${id}`);
}),
);
behaves exactly the same as:
const items = ids.pipe(
concatMap(id => {
if (!id) {
throw new Error("invalid ID");
}
return http.get(`https://database.xyz/items?id=${id}`);
}),
);
Seeing that equivalence, my friend understandably wondered if the two forms were always identical. No, they aren’t. Consider the following two expressions:
The type of the first expression is Observable<number>
, which makes perfect sense: it’s a stream of numbers that might or might not err out.
The type of the second expression is Observable<Observable<unknown> |number>
, a construct about which the kindest thing I can say is that it probably won’t do what you want.
So why did it work in the earlier case? Because of the concatMap()
! It looks irrelevant, but remember: concatMap subscribes to the value returned from its function. Since that was the doomed Observable returned from throwError()
, it failed just as if an error or exception had been thrown from the function.
Those subscriptions don’t always happen though. In fact, they only happen in flattening operators:concatMap()
, mergeMap()
, switchMap()
, and exhaustMap()
. All the regular operators, like map()
, scan()
, and filter()
, do not do this. They just pass on the Observable to the next function. Consider the following:
That error in the first map? It’s just discarded — or rather, it’s transformed into a 1
, without ever being unwrapped into a real exception.
Is there any reason to use throwError()
inside a pipe? As a function, it can be used in an expression, so there’s an aesthetic argument. Consider this:
It is, arguably, more readable than a statement-based equivalent. You have to compare that advantage against the risk of luring another programmer of trying the same trick somewhere it won’t work, like in a map()
.
Overall, I would advise against ever using throwError()
inside pipes, even in cases where it would technically work. It just confuses people. Use it for what it was designed for: creating Observables that immediately fail.
spring summer autumn winter
he sang his didn’t he danced his did.
— Town, e e cummings, 1923