-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(onUnhandledError): configuration point added for unhandled errors
- Adds new configuration setting `onUnhandledError`, which defaults to using "hostReportError" behavior. - Adds tests that ensure it is called appropriately, and that it is always asynchronous. - Updates internal name of empty observer to be `EMPTY_OBSERVER` throughout and types it to prevent mutations. Reduces overhead by using the `noop` function for its callbacks. - Errors that occur during subscription setup _after_ the subscription was already closed will no longer log to `console.warn` BREAKING CHANGE: Errors that occur during setup of an observable subscription after the subscription has emitted an error or completed will now throw in their own call stack. Before it would call `console.warn`. This is potentially breaking in edge cases for node applications as a node app may be configured to crash for an unhandled exception. In the unlikely event this affects you, you can configure the behavior to `console.warn` in the new configuration setting like so: `import { config } from 'rxjs'; config.onUnhandledError = (err) => console.warn(err);`
- Loading branch information
Showing
12 changed files
with
244 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,168 @@ | ||
/** @prettier */ | ||
|
||
import { config } from '../src/internal/config'; | ||
import { expect } from 'chai'; | ||
import { Observable } from 'rxjs'; | ||
|
||
describe('config', () => { | ||
it('should have a Promise property that defaults to nothing', () => { | ||
expect(config).to.have.property('Promise'); | ||
expect(config.Promise).to.be.undefined; | ||
}); | ||
|
||
describe('onUnhandledError', () => { | ||
afterEach(() => { | ||
config.onUnhandledError = null; | ||
}); | ||
|
||
it('should default to null', () => { | ||
expect(config.onUnhandledError).to.be.null; | ||
}); | ||
|
||
it('should call asynchronously if an error is emitted and not handled by the consumer observer', (done) => { | ||
let called = false; | ||
const results: any[] = []; | ||
|
||
config.onUnhandledError = err => { | ||
called = true; | ||
expect(err).to.equal('bad'); | ||
done() | ||
}; | ||
|
||
const source = new Observable<number>(subscriber => { | ||
subscriber.next(1); | ||
subscriber.error('bad'); | ||
}); | ||
|
||
source.subscribe({ | ||
next: value => results.push(value), | ||
}); | ||
expect(called).to.be.false; | ||
expect(results).to.deep.equal([1]); | ||
}); | ||
|
||
it('should call asynchronously if an error is emitted and not handled by the consumer next callback', (done) => { | ||
let called = false; | ||
const results: any[] = []; | ||
|
||
config.onUnhandledError = err => { | ||
called = true; | ||
expect(err).to.equal('bad'); | ||
done() | ||
}; | ||
|
||
const source = new Observable<number>(subscriber => { | ||
subscriber.next(1); | ||
subscriber.error('bad'); | ||
}); | ||
|
||
source.subscribe(value => results.push(value)); | ||
expect(called).to.be.false; | ||
expect(results).to.deep.equal([1]); | ||
}); | ||
|
||
it('should call asynchronously if an error is emitted and not handled by the consumer in the empty case', (done) => { | ||
let called = false; | ||
config.onUnhandledError = err => { | ||
called = true; | ||
expect(err).to.equal('bad'); | ||
done() | ||
}; | ||
|
||
const source = new Observable(subscriber => { | ||
subscriber.error('bad'); | ||
}); | ||
|
||
source.subscribe(); | ||
expect(called).to.be.false; | ||
}); | ||
|
||
it('should call asynchronously if a subscription setup errors after the subscription is closed by an error', (done) => { | ||
let called = false; | ||
config.onUnhandledError = err => { | ||
called = true; | ||
expect(err).to.equal('bad'); | ||
done() | ||
}; | ||
|
||
const source = new Observable(subscriber => { | ||
subscriber.error('handled'); | ||
throw 'bad'; | ||
}); | ||
|
||
let syncSentError: any; | ||
source.subscribe({ | ||
error: err => { | ||
syncSentError = err; | ||
} | ||
}); | ||
|
||
expect(syncSentError).to.equal('handled'); | ||
expect(called).to.be.false; | ||
}); | ||
|
||
it('should call asynchronously if a subscription setup errors after the subscription is closed by a completion', (done) => { | ||
let called = false; | ||
let completed = false; | ||
|
||
config.onUnhandledError = err => { | ||
called = true; | ||
expect(err).to.equal('bad'); | ||
done() | ||
}; | ||
|
||
const source = new Observable(subscriber => { | ||
subscriber.complete(); | ||
throw 'bad'; | ||
}); | ||
|
||
source.subscribe({ | ||
error: () => { | ||
throw 'should not be called'; | ||
}, | ||
complete: () => { | ||
completed = true; | ||
} | ||
}); | ||
|
||
expect(completed).to.be.true; | ||
expect(called).to.be.false; | ||
}); | ||
|
||
/** | ||
* Thie test is added so people know this behavior is _intentional_. It's part of the contract of observables | ||
* and, while I'm not sure I like it, it might start surfacing untold numbers of errors, and break | ||
* node applications if we suddenly changed this to start throwing errors on other jobs for instances | ||
* where users accidentally called `subscriber.error` twice. Likewise, would we report an error | ||
* for two calls of `complete`? This is really something a build-time tool like a linter should | ||
* capture. Not a run time error reporting event. | ||
*/ | ||
it('should not be called if two errors are sent to the subscriber', (done) => { | ||
let called = false; | ||
config.onUnhandledError = () => { | ||
called = true; | ||
}; | ||
|
||
const source = new Observable(subscriber => { | ||
subscriber.error('handled'); | ||
subscriber.error('swallowed'); | ||
}); | ||
|
||
let syncSentError: any; | ||
source.subscribe({ | ||
error: err => { | ||
syncSentError = err; | ||
} | ||
}); | ||
|
||
expect(syncSentError).to.equal('handled'); | ||
// This timeout would be scheduled _after_ any error timeout that might be scheduled | ||
// (But we're not scheduling that), so this is just an artificial delay to make sure the | ||
// behavior sticks. | ||
setTimeout(() => { | ||
expect(called).to.be.false; | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Observer } from './types'; | ||
import { config } from './config'; | ||
import { reportUnhandledError } from './util/reportUnhandledError'; | ||
import { noop } from './util/noop'; | ||
|
||
/** | ||
* The observer used as a stub for subscriptions where the user did not | ||
* pass any arguments to `subscribe`. Comes with the default error handling | ||
* behavior. | ||
*/ | ||
export const EMPTY_OBSERVER: Readonly<Observer<any>> = { | ||
closed: true, | ||
next: noop, | ||
error(err: any): void { | ||
if (config.useDeprecatedSynchronousErrorHandling) { | ||
throw err; | ||
} else { | ||
reportUnhandledError(err); | ||
} | ||
}, | ||
complete: noop | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/** @prettier */ | ||
import { config } from '../config'; | ||
|
||
/** | ||
* Handles an error on another job either with the user-configured {@link onUnhandledError}, | ||
* or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc. | ||
* | ||
* This should be called whenever there is an error that is out-of-band with the subscription | ||
* or when an error hits a terminal boundary of the subscription and no error handler was provided. | ||
* | ||
* @param err the error to report | ||
*/ | ||
export function reportUnhandledError(err: any) { | ||
setTimeout(() => { | ||
const { onUnhandledError } = config; | ||
if (onUnhandledError) { | ||
// Execute the user-configured error handler. | ||
onUnhandledError(err); | ||
} else { | ||
// Throw so it is picked up by the runtime's uncaught error mechanism. | ||
throw err; | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters