Skip to content

Commit 96256e4

Browse files
committed
Error handling improvements
Proposing some changes that I mentioned [here](#236 (comment)). To recap, I think the error logging in Stimulus is great, but several people (me included) have been caught by the fact that errors aren't reported elsewhere out of the box. The aim of this PR is to automatically integrate with third party error tracking services where possible, and to improve the documentation where not. Specific changes: - If `window.onerror` is defined, Stimulus will now call it after logging an error. Many error tracking tools define this method, eg. [Sentry](https://github.com/getsentry/sentry-javascript/blob/0ee07995d415d3870608c477cbdcf8445a51e1bb/packages/browser/src/loader.js#L192), [Airbrake](https://github.com/airbrake/airbrake-js/blob/9d4787b1c559aa39107d7288f46c4108c9a9d954/packages/browser/src/notifier.ts#L70) - Added documentation on how error handling works, including what happens out of the box and how to override it (with code sample from #53) - Added tests for the error handler.
1 parent c47f551 commit 96256e4

File tree

4 files changed

+85
-1
lines changed

4 files changed

+85
-1
lines changed

docs/handbook/07_installing_stimulus.md

+20
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,29 @@ Stimulus supports all evergreen, self-updating desktop and mobile browsers out o
120120
If your application needs to support older browsers like Internet Explorer 11, include the [`@stimulus/polyfills`](https://www.npmjs.com/package/@stimulus/polyfills) package before loading Stimulus.
121121

122122
```js
123+
// src/application.js
123124
import "@stimulus/polyfills"
124125
import { Application } from "stimulus"
125126

126127
const application = Application.start()
127128
//
128129
```
130+
131+
## Error handling
132+
133+
All calls from Stimulus to your application's code are wrapped in a `try ... catch` block.
134+
135+
If your code throws an error, it will be caught by Stimulus and logged to the browser console, including extra detail such as the controller name and event or lifecycle function being called. If you use an error tracking system that defines `window.onerror`, Stimulus will also pass the error on to it.
136+
137+
You can override how Stimulus handles errors by defining `Application#handleError`:
138+
139+
```js
140+
// src/application.js
141+
import { Application } from "stimulus"
142+
const application = Application.start()
143+
144+
application.handleError = (error, message, detail) => {
145+
console.warn(message, detail)
146+
Raven.captureException(error)
147+
}
148+
```

packages/@stimulus/core/src/application.ts

+2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export class Application implements ErrorHandler {
7070

7171
handleError(error: Error, message: string, detail: object) {
7272
this.logger.error(`%s\n\n%o\n\n%o`, message, error, detail)
73+
74+
window.onerror && window.onerror(message, "", 0, 0, error)
7375
}
7476
}
7577

packages/@stimulus/core/src/tests/modules/class_tests.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ControllerTestCase } from "../cases/controller_test_case"
22
import { ClassController } from "../controllers/class_controller"
33

4-
export default class ValueTests extends ControllerTestCase(ClassController) {
4+
export default class ClassTests extends ControllerTestCase(ClassController) {
55
fixtureHTML = `
66
<div data-controller="${this.identifier}"
77
data-${this.identifier}-active-class="test--active"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Controller } from "../.."
2+
import { Application } from "../../application"
3+
import { ControllerTestCase } from "../cases/controller_test_case"
4+
5+
class MockLogger {
6+
errors: any[] = []
7+
logs: any[] = []
8+
warns: any[] = []
9+
10+
log(event: any) {
11+
this.logs.push(event)
12+
}
13+
14+
error(event: any) {
15+
this.errors.push(event)
16+
}
17+
18+
warn(event: any) {
19+
this.warns.push(event)
20+
}
21+
}
22+
23+
class ErrorWhileConnectingController extends Controller {
24+
connect() {
25+
throw new Error('bad!');
26+
}
27+
}
28+
29+
class TestApplicationWithDefaultErrorBehavior extends Application {
30+
}
31+
32+
export default class ErrorHandlerTests extends ControllerTestCase(ErrorWhileConnectingController) {
33+
controllerConstructor = ErrorWhileConnectingController
34+
35+
async setupApplication() {
36+
const logger = new MockLogger()
37+
38+
this.application = new TestApplicationWithDefaultErrorBehavior(this.fixtureElement, this.schema)
39+
this.application.logger = logger
40+
41+
window.onerror = function(message, source, lineno, colno, error) {
42+
logger.log(`error from window.onerror. message = ${message}, source = ${source}, lineno = ${lineno}, colno = ${colno}`)
43+
}
44+
45+
await super.setupApplication()
46+
}
47+
48+
async "test errors in connect are thrown and handled by built in logger"() {
49+
const mockLogger: any = this.application.logger
50+
51+
// when `ErrorWhileConnectingController#connect` throws, the controller's application's logger's `error` function
52+
// is called; in this case that's `MockLogger#error`.
53+
this.assert.equal(1, mockLogger.errors.length)
54+
}
55+
56+
async "test errors in connect are thrown and handled by window.onerror"() {
57+
const mockLogger: any = this.application.logger
58+
59+
this.assert.equal(1, mockLogger.logs.length)
60+
this.assert.equal('error from window.onerror. message = Error connecting controller, source = , lineno = 0, colno = 0', mockLogger.logs[0])
61+
}
62+
}

0 commit comments

Comments
 (0)