RxJS version:
5.4.0
Code to reproduce:
const { Observable } = require('rxjs');
function myBigActionChainBuilder(){
//handle requests from the server, routing them to handlers in
//different files depending on what url we are serving.
return Observable.of({})
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => { return; })
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
.mergeMap((state) => Observable.of(state))
}
myBigActionChainBuilder().subscribe(()=>{
//do nothing
})
Feature Request:
When I run the above code, it gives me this error, which is expected.
TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
But the stack trace, even if I insert Error.stackTraceLimit = Number.MAX_SAFE_INTEGER;, only gives me the stack trace to my subscribe call. This is useless, as everyone has acknowledged, but I am surprised that no one has come up with an alternative. Therefore I would like to propose the solution that has worked very well for me.
Simply add this line to the Observable constructor this.conStack = new Error().stack;, and change the subscribe method so it looks like the below.
subscribe(observerOrNext, error, complete) {
var operator = this.operator;
var sink = toSubscriber(observerOrNext, error, complete);
//add this:
if (sink) {
const myErr = sink.error;
const self = this;
sink.error = function (err) {
console.log('Observable error', err.message, self.conStack);
return myErr.apply(this, arguments);
}
}
//to here
... the rest of subscribe ...
This returns a much more useful stack trace that points to the actual operator call where the error was thrown. Here is an example stack trace from different code than the one above. As you can see the 4th item in the trace is the method that caused the error.
Observable error You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable. Error
at new Observable (...\Rx.js:858:29)
at ScalarObservable.Observable.lift (...\Rx.js:871:33)
at ScalarObservable.switchMap (...\Rx.js:13299:21)
at $tw.boot.startup (...\boot.js:2125:27)
at \boot.js:2434:12
at $tw.boot.decryptEncryptedTiddlers (...\boot.js:1468:2)
at $tw.boot.boot [as boot] (...\boot.js:2432:11)
at Object.<anonymous> (...\tiddlywiki.js:13:10)
The downside to doing something like this is you'd end up logging every error, even if it was handled later. Probably worse than that, you're adding the overhead of creating an Error and putting a large string in memory for every Observable instance.
I think we can definitely do more here, though. It's a hard problem to solve.
But wouldn't there be a way to selectively enable it? Are there other development features that are enabled in a certain way? Like you can enable it for development and then disable it for production. I guess it could be modified at runtime in a global way like add/operator files do.
And we don't need to log every error. That can be fixed! Please don't ignore this issue just for that! :)
We're always open to suggestions. But there's also a lot on our plates. Keep coming up with great ideas and maybe one of them will stick.
Would it be possible to add a global variable of some sort that would allow this to be turned on or off?
I agree the call stacks should give more steps as possible. I just encounter an error, while the error has some useful stacks but call stack on debug panel just has Rx callbacks.
Error message
TypeError: Cannot read property 'sma_type' of undefined
at someFunction (a.js:616)
at VueComponent.abcFunction (b.js:1058)
at VueComponent.boundFn [as abcFunction] (vue.esm.js:189)
at VueComponent.defFunction (b.js:1232)
at VueComponent.boundFn [as defFunction] (vue.esm.js:189)
at b:1197
at Array.map (<anonymous>)
at VueComponent.xyzFunction (b.js:1196)
at VueComponent.boundFn [as xyzFunction] (vue.esm.js:189)
at SafeSubscriber._next (b.js:683)
at SafeSubscriber.__tryOrUnsub (Subscriber.js:239)
at SafeSubscriber.next (Subscriber.js:186)
at Subscriber._next (Subscriber.js:126)
at Subscriber.next (Subscriber.js:90)
at FilterSubscriber._next (filter.js:89)
at FilterSubscriber.Subscriber.next (Subscriber.js:90)
And then the call stack

I can't locate real position cause bug, which mean it's not possible to debug this problem. All I can do is continue and find information from console which has thrown error message.
the problem here is tricky. It's that libraries like RxJS generally take some users function, then execute it internally. If there's an error, it then sends the error down the observer chain via error handlers. If the error is unhandled, it's rethrown.. All of this means it's not easy to for JavaScript engines to give pretty callstacks like people might want.
Thus far the best idea I've had going forward, is to actually throw a new error that contains the stringified function that was executed that caused the error the be thrown. Hopefully that would help people find what's causing the error. The major downside to this is that minified code is still going to be hard to read.
I think there should be a development mode that records and prints the stack traces from the operator function that does the "lift". So it would pinpoint which function the problem is at.
The main place this would benefit is where the function of a concatMap does not return an observable.
This is a huge problem. I'm ready to give up on RxJS because I cannot have obscure impossible to find bugs in a huge code base with no reasonable way to pin point it.
@louispvb are you using rxjs 5 or 6? I have a feeling that the way 6 works should make it a lot simpler to debug. At the very least you would only have to override to the pipe method to add whatever control logic you need. I haven't done much recently though, so I can't really say. I agree that it is frustrating.
Also, if an error throws inside an operator the stack trace should point to it. The problem of course shows up if you throw a string instead of an error object.
According to the docs of rxjs v7 a goal is to make stack traces shorter. Could any of the implementers shed some light on how they want to achieve this? I know of the trx-branch which seems promising. Is this the planned way to go?
Most helpful comment
According to the docs of rxjs v7 a goal is to make stack traces shorter. Could any of the implementers shed some light on how they want to achieve this? I know of the trx-branch which seems promising. Is this the planned way to go?