You can throw an error at any point in your stream. The following snippet will throw an error if results.data doesn't contain a Guild with the passed in guildId. Now your result will either be defined (Not null/undefined), or an error;
public getGuild(guildId: string, token: string): Observable<Guild> {
return this.httpService.get<Guild[]>(url, {
headers: { authorization: token },
}).pipe(
map(result =>
result.data.find(guild => guild.id === guildId)
),
tap(guild => {
if(guild == null) throw new Error(`No guild with ${guildId} found`);
})
);
}
Idiomatic RxJS
tap takes a stream and returns the same stream unaltered. It can be seen as the same as map(x => x) - Some people believe idiomatic RxJS should preclude tap from having any control-flow in a program.
Where this often makes sense are in situations where users are tempted to set a global variable and then make a choice based on that value later.
from(something).pipe(
tap(x => {
if(x < 10) this.globalValue = 0;
if(x > 9) this.globalValue = 25;
else this.globalValue = 75;
}),
mergeMap(x => this.service.save(x, this.globalValue))
);
In this case, your stream is no longer 'pure' as you can't concretely reason about what this.globalValue will be (Perhaps some other block of code alters this global value again before you read it). This makes things hard to test. You lose much of the benefits of RxJS functional style.
All these problems are the same if you replace tap(x => doSomething) with map(x => {doSomething; return x}) - they're the same set of issues. However, idiomatically, a programmer may spend more time understanding what map is doing because they may expect tap to have no control flow.
The subjective part
In my view, tap is a great place to throw an error. It should tell any developer reading/updating the code that "this error is produced by the shape of the value at this point in the stream". Things like validation and logging fit very well (idiomatically) into RxJS' tap operator. Throwing an error during validation is fairly natural.
How would you write a custom assertion operator (You could strip them all for production builds without worrying that you're ruining anything)
function assert<T>(fn: (x: T) => boolean): MonoTypeOperatorFunction<T>{
return s => s.pipe(
tap(val => {
if(!fn(val)) throw new Error("Assertion Failed");
})
);
}
An implementation using tap is a clear standout for clarity. You also don't run into any idiomatic problems during use since the word tap doesn't appear in your stream.
public getGuild(guildId: string, token: string): Observable<Guild> {
return this.httpService.get<Guild[]>(url, {
headers: { authorization: token },
}).pipe(
map(result =>
result.data.find(guild => guild.id === guildId)
),
assert(result => result != null)
);
}
That's pretty clear, I'd say. We could work on a better error message though.
Another way to avoid the debate
If you'd rather not use tap that way, here's another way to write it in fairly idiomatically correct RxJS.
public getGuild(guildId: string, token: string): Observable<Guild> {
return this.httpService.get<Guild[]>(url, {
headers: { authorization: token },
}).pipe(
map(result => {
const guild = result.data.find(guild => guild.id === guildId);
if(guild == null) throw new Error(`No guild with ${guildId} found`);
return guild;
})
);
}
Now - the error is thrown as a "I failed to map result: Guild[] to guild: Guild" error.
console.log(guild)? You should see something like Observable { _isScalar: false }. It will never be undefined becauseguildis a reference to your observable and not a reference to the return value(s) of your observable. - Mrk Sef