The Difference Between throwError and throw

The American poet Edward Estlin Cummings (1894–1962) is remembered for his unconventional approach to typography and punctuation, often rendering his name in lowercase as e e cummings. One can only imagine his reaction to a question a colleague put to me about RxJS: "Does returning throwError amount to the same thing as writing 'throw error'?"

The concise answer (whatever e e might think) is no. They are fundamentally distinct, yet the fact that this question came up is worth exploring.

throwError() is a function; specifically, it is a utility that produces an Observable which immediately emits an error notification.

Consider a scenario where you have a function that accepts an ID and queries a remote database with that ID, perhaps like this:

function getItem(id: string): Observable<Item> {
  return http.get(`https://database.xyz/items?id=${id}`);
}

Over time, you observe that the function is frequently invoked with an empty ID. The backend will eventually reject such requests, but you end up waiting needlessly. There are numerous strategies you could employ, but the most effective might be

By making this adjustment, the function's interface stays consistent: it continues to return an Observable, and when the request fails for any reason, the Observable errors out. The only change is that with an invalid ID, it fails immediately, avoiding unnecessary load on the server. This is precisely the scenario throwError was designed for.

Throwing an error, by contrast, is an entirely different mechanism. It isn't a function at all; it's a statement that halts the normal execution flow. If you had decided to modify getItem() to throw an error instead, like so:

function getItem(id: string): Observable<Item> {
  if (!id) {
    throw new Error("invalid ID");
  }
  return http.get(`https://database.xyz/items?id=${id}`);
}

That appears similar at first glance, but it's a poor choice. It effectively alters the API: someone will need to locate every call site, add catch blocks, and refactor the code to deal with the new exception—while also retaining all the old error-handling logic for situations where the network request is attempted but fails.

Why did my colleague wonder whether these two distinct constructs were equivalent? He had observed instances where both patterns were used interchangeably. Suppose you weren't dealing with a getItem() function, but instead needed to transform a stream of IDs into a stream of Items. This implementation:

const items = ids.pipe(
  concatMap(id => {
    if (!id) {
      return throwError("invalid ID");
    }
    return http.get(`https://database.xyz/items?id=${id}`);
  }),
);

operates identically to:

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, it's understandable that he questioned whether these forms are always interchangeable. They are not. Examine these two expressions:

The first expression has the type Observable<number>, which is logical: it represents a stream of numbers that may or may not produce an error.

The second expression has the type Observable<Observable<unknown> |number>, a structure that is best described as unlikely to behave as expected.

So why did it function correctly in the earlier example? The answer lies with concatMap()! It may seem inconsequential, but concatMap subscribes to the value returned from its callback. Since that value was the erroring Observable from throwError(), it emitted an error just as if an exception had been thrown inside the function.

However, such subscriptions don't always occur. In fact, they only happen in flattening operators: concatMap(), mergeMap(), switchMap(), and exhaustMap(). Regular operators—such as map(), scan(), and filter()—don't subscribe to returned Observables. They simply pass the Observable along to the next operator. Consider this example:

That error inside the first map()? It's essentially ignored—or rather, it gets converted into a 1, never being unwrapped into an actual exception.

Is there any justification for using throwError() inside a pipe? Since it's a function, it can be used within an expression, which offers an aesthetic appeal. Look at this:

It is, arguably, more elegant than a statement-based approach. Yet you must weigh that benefit against the danger of encouraging another developer to attempt the same technique in a context where it won't work, such as within a map().

In general, I would caution against using throwError() inside pipes at all, even in situations where it might technically succeed. It only creates confusion. Reserve it for its intended purpose: constructing Observables that fail right away.

spring summer autumn winter
he sang his didn’t he danced his did.
Town, e e cummings, 1923