You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have recently set up a controller where we want to ensure that a specific target exists and found what could be a bug in how the error handling works in Stimulus.
All calls from Stimulus to your application’s code are wrapped in a try ... catch block.
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.
Problem
Errors in the <some>valueChanged callbacks are not being caught like all other errors and result in code that is hard to debug, log and test.
Steps to reproduce (basic)
Set up a simple controller with a value and throw any error inside the value changed callback.
// reveal-controller.jsimport{Controller}from'@hotwired/stimulus';exportdefaultclassextendsController{staticvalues={closed: Boolean};closedValueChanged(){thrownewError('Is this error caught?');}}
Add the HTML to connect to the controller & load in a browser.
Expected: Error should be handled and captured similar to errors in other methods. Actual: Error is not caught.
// console result
stimulus.js:2023 application #starting
stimulus.js:2023 reveal #initialize
reveal-controller.js:7 Uncaught (in promise) Error: Is this error caught?
at t.value (reveal-controller.js:7:11)
at M.invokeChangedCallback (stimulus.js:1115:31)
at M.invokeChangedCallbacksForDefaultValues (stimulus.js:1100:22)
at M.start (stimulus.js:1057:14)
at B.connect (stimulus.js:1376:28)
at D.connectContextForScope (stimulus.js:1550:17)
at H.scopeConnected (stimulus.js:1924:20)
at j.elementMatchedValue (stimulus.js:1840:27)
at A.tokenMatched (stimulus.js:946:27)
at S.tokenMatched (stimulus.js:877:23)
value @ reveal-controller.js:7
invokeChangedCallback @ stimulus.js:1115
invokeChangedCallbacksForDefaultValues @ stimulus.js:1100
start @ stimulus.js:1057
connect @ stimulus.js:1376
As a comparison, using this HTML and clicking the button will result in a caught error.
// reveal-controller.jsimport{Controller}from'@hotwired/stimulus';exportdefaultclassextendsController{staticvalues={closed: Boolean};close(){thrownewError('Is this error caught?');}}
Observe that there is no toggle target set in the HTML, accessing this in the controller will cause a caught error (as expected).
stimulus.js:2023 application #starting
stimulus.js:2023 reveal #initialize
stimulus.js:2018 Error connecting controller
Error: Missing target element "toggle" for "reveal" controller
at t.get (stimulus.js:2154:27)
at t.value (reveal-controller.js:18:10)
at t.value (reveal-controller.js:8:44)
at B.connect (stimulus.js:1380:29)
at D.connectContextForScope (stimulus.js:1550:17)
at H.scopeConnected (stimulus.js:1924:20)
at j.elementMatchedValue (stimulus.js:1840:27)
at A.tokenMatched (stimulus.js:946:27)
at S.tokenMatched (stimulus.js:877:23)
at stimulus.js:871:40
{identifier: 'reveal', controller: t, element: div.container}
handleError @ stimulus.js:2018
handleError @ stimulus.js:1424
connect @ stimulus.js:1384
connectContextForScope @ stimulus.js:1550
...
Now, refactor this same controller to use value changed callbacks.
import{Controller}from'@hotwired/stimulus';exportdefaultclassextendsController{staticvalues={closed: Boolean};statictargets=['content','toggle'];/** * Will cause uncaught error if a target does not exist. */closedValueChanged(shouldClose){if(shouldClose){this.toggleTarget.setAttribute('aria-expanded','false');this.contentTarget.hidden=true;}else{this.toggleTarget.setAttribute('aria-expanded','true');this.contentTarget.hidden=false;}}close(){this.closedValue=true;}open(){this.closedValue=false;}toggle(){this.closedValue=!this.closedValue;}}
With the same HTML, we now get an uncaught error, this does not get handled by Stimulus, not logged as expected and also makes it very difficult to unit test this scenario. The message is there but there is no way for this to be caught by any code easily.
stimulus.js:2023 application #starting
stimulus.js:2023 reveal #initialize
stimulus.js:2154 Uncaught (in promise) Error: Missing target element "toggle" for "reveal" controller
at t.get (stimulus.js:2154:27)
at t.value (reveal-controller.js:15:12)
at M.invokeChangedCallback (stimulus.js:1115:31)
at M.invokeChangedCallbacksForDefaultValues (stimulus.js:1100:22)
at M.start (stimulus.js:1057:14)
at B.connect (stimulus.js:1376:28)
at D.connectContextForScope (stimulus.js:1550:17)
at H.scopeConnected (stimulus.js:1924:20)
at j.elementMatchedValue (stimulus.js:1840:27)
at A.tokenMatched (stimulus.js:946:27)
get @ stimulus.js:2154
value @ reveal-controller.js:15
invokeChangedCallback @ stimulus.js:1115
invokeChangedCallbacksForDefaultValues @ stimulus.js:1100
start @ stimulus.js:1057
connect @ stimulus.js:1376
connectContextForScope @ stimulus.js:1550
scopeConnected @ stimulus.js:1924
I can kind of work around this by doing a check of targets that we care about in the initalize callback (called before the first changed callback).
initialize(){// attempt to 'fail early' if the controller is misconfiguredthis.toggleTarget;}
We still get the uncaught error though, but at least we get a caught one just before it. However, this is not going to help when unit testing this case as the process will still fail.
Potential solution
I think the issue is that the private method invokeChangedCallback does not pass the error to handleError. Instead it just throws it, but I cannot see any places where this method is called that catch this error, causing it to bubble up.
I have recently set up a controller where we want to ensure that a specific target exists and found what could be a bug in how the error handling works in Stimulus.
According to the documentation on error handling;
Problem
Errors in the
<some>valueChanged
callbacks are not being caught like all other errors and result in code that is hard to debug, log and test.Steps to reproduce (basic)
Steps to reproduce (full details)
Consider this controller code.
And this HTML.
Observe that there is no
toggle
target set in the HTML, accessing this in the controller will cause a caught error (as expected).Now, refactor this same controller to use value changed callbacks.
With the same HTML, we now get an uncaught error, this does not get handled by Stimulus, not logged as expected and also makes it very difficult to unit test this scenario. The message is there but there is no way for this to be caught by any code easily.
I can kind of work around this by doing a check of targets that we care about in the initalize callback (called before the first changed callback).
We still get the uncaught error though, but at least we get a caught one just before it. However, this is not going to help when unit testing this case as the process will still fail.
Potential solution
I think the issue is that the private method
invokeChangedCallback
does not pass the error tohandleError
. Instead it just throws it, but I cannot see any places where this method is called that catch this error, causing it to bubble up.stimulus/src/core/value_observer.ts
Lines 98 to 103 in 7b810ec
Maybe we can avoid this by passing the error to the
handleError
instead as this pattern is used by other code.Note: I have not tested this but just a guess.
The text was updated successfully, but these errors were encountered: