thomashoneyman/purescript-halogen-hooks

Runtime-Error "Uncaught TypeError: Cannot read property 'value0' of undefined"

CarstenKoenig opened this issue · 11 comments

Hi,

sadly this one is rather complex.
I have an application that does various async stuff on init (loading local storage, querying the backend, etc.) and is using RoutingDuplex for SPA navigation.

There error happens when I reload a certain page - if I navigate there from within the application (so I guess no initial logic) the error does not happen.


The error is thrown in the last line here:

           if (reason instanceof Halogen_Hooks_Internal_Eval_Types.Step) {
                                  return Control_Bind.bind(Control_Monad_Free.freeBind)(getState)(function (v1) {
                                      var nextIndex = (function () {
                                          var $69 = (v1.effectCells.index + 1 | 0) < Data_Array.length(v1.effectCells.queue);
                                          if ($69) {
                                              return v1.effectCells.index + 1 | 0;
                                          };
                                          return 0;
                                      })();
                                      var v2 = unsafeGetCell(v1.effectCells.index)(v1.effectCells.queue);
                                      if (v.value0 instanceof Data_Maybe.Just && v2.value0 instanceof Data_Maybe.Just) {

it's the v2 refernce that is undefined

This code seems to be part of the UseState constructor of the HookM Monad/Algebra if this help.


I'm trying to get a minimal example that causes this problem but right now I could not come up with one.

Is there anything more I can do to help you?


a part of the stack-trace looks like this

    at app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:13095
    at $tco_loop (app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8903)
    at toView (app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8917)
    at go (app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8964)
    at app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:130
    at $tco_loop (app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8903)
    at toView (app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8917)
    at go (app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8983)
    at go (app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5761)
    at app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5766
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:13095
$tco_loop @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8903
toView @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8917
go @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8964
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:130
$tco_loop @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8903
toView @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8917
go @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:8983
go @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5761
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5766
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4449
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4813
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5649
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5291
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5291
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:27738
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:27740
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:27752
renderComponentSlot @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:28168
render @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:28174
patch @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:28194
step @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:6694
patchWidget @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:11915
step @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:6694
onThese @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:12002
exports.diffWithIxE @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:6755
patchElem @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:12011
patchElem @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:11981
step @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:6694
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:28232
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:27800
runSync @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4261
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4499
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4813
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4567
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4519
drain @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4291
enqueue @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4312
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4510
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5810
xhr.onload @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:69
setTimeout (async)
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4699
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4813
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5649
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5291
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5291
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:27738
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:27740
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:27752
renderComponentSlot @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:28168
render @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:28174
patch @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:28194
step @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:6694
patchWidget @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:11915
step @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:6694
onThese @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:12002
exports.diffWithIxE @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:6755
patchElem @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:12011
patchElem @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:11981
step @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:6694
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:28232
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:27800
runSync @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4261
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4499
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4813
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4567
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4519
drain @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4291
enqueue @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4312
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4510
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5810
xhr.onload @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:69
load (async)
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:68
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5807
runAsync @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4269
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4504
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4813
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4567
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4519
drain @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4291
enqueue @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4312
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4510
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5810
xhr.onload @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:69
load (async)
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:68
__do @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:5807
runAsync @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4269
run @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4504
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4519
drain @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4291
enqueue @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4312
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:4510
(anonymous) @ app.js?v=LYwJKuTZn4ugPIuYz3BTJgLs2n8z4EpFjM4kEf_aYfk:6915
Show 11 more frames

Just an update - I think I've got the problem and it was most likely my own fault:

I used a captures on an component/hook-input together with useTickEffect seems this fired the Step before the Initialize and so the effectsQueue is empty.

So is this a bug or expected behavior - to be honest: I don't know - what do you think?

Could you provide a reproducible demo of the issue?

Thanks for the report, @CarstenKoenig! I'll need to take a deeper look when I have a little more time, but this library definitely shouldn't produce runtime errors unless you explicitly do something unsafe, so I consider this a bug that needs to be fixed.

I'll try to recreate a short example - I let you know if I can come up with something quick.

Hooks can be run on try.purescript.org, so some code that fails over there would be ideal for quickly debugging in a shared environment. Thank you!

I tried to recreate it locally - it's actually really hard - a simple nested componente with a simulated Aff (setTimeout to delay a bit) does not recreate the error.
I guess I have to dig through the actual code some more to see what is needed - but this has to wait for tomorrow - it's quite late here sorry.

short update:

there is something really strange going on.

I did not manage to reproduce with a small example, but in my big application I ran into yet another (probably related) problem: it can happen, that i get a Tuple undefined 0 when I do Hooks.useState (I checked with that Debug.log function).

Just some hints about what might be happening:

  • the app uses routing and on when a route is matched (uses matchesWith from Routing.PushState) it tells the main-component (could call it routing component I guess) about the new route.
  • the main/routing component will display the pages using slots based on the current route (that might change via the query)
  • now during each query there might be an affjax call (to fetch a token if there was none) and the Hooks.put for the current-route is after this Aff-call

Now in exactly this case: the affjax-call happens it seems that the useState in the component that will eventually be shown will trigger a few times when it will return an undefined/0 Tuple - if the call does not happen everything is fine (useState will correctly initialize with the given value - no undefined)

In my use case this will trigger a match-exception (I'm using the RemoteData library and it has this in it's functor-map instance:

throw new Error("Failed pattern match at Data.RemoteData (line 12, column 1 - line 12, column 90): " + [ showData.constructor.name, v.constructor.name ]);

the original problem seems to be similar - there a affjax-call was waited for inside a useTickEffect - if I move this into useLifecycleEffect it does not happen anymore.


Has somebody any clue where I can look further?

BTW versions used:

  • halogen - v.5.1.0
  • halogen-hooks - v.0.4.3

tried updating the package-set (now halogen is v.5.1.1) - did not help :(

I narrowed it down some more - please decide if this is an bug or it's me being stupid.


Background: I needed some way to keep track of logged-in users so my idea was to use a Handle-Pattern, put the Handle into a reader-monad (to pass it around) and be somewhat clever with IxQueue to create a EventSource components can subscribe to when the status of the login changes.

The problem here seems to be, that the affjax-call I mentioned eventually will update this status and trigger the eventsource and the main/routing component is subscribed to that (I need to know if the current users has certain rights to give access to restricted routes) - that seems to have caused the issue here (if I remove the emit or something similar the bug will go away).


Should I still try to create some sort of minimum-example? It probably would involve all that.


Some details on the implementation

this is the implementation in case someone want to take a look

module Services.Login.Handle
  ( Handle
  , init
  , subscribe
  , putToken
  , readToken
  ) where

import Prelude

import Control.Error.Util (hush)
import Data.Maybe (Maybe)
import Effect.Aff.Class (class MonadAff)
import Effect.Class (class MonadEffect, liftEffect)
import Effect.Ref as Ref
import Halogen (SubscriptionId)
import Halogen.Hooks (HookM)
import Halogen.Hooks as Hooks
import Halogen.Query.EventSource (EventSource, Finalizer(..), effectEventSource, emit)
import IxQueue as Queue
import Services.Login.Token (JwtToken)

newtype Handle
  = Handle
  { setToken :: forall m. MonadEffect m => Maybe JwtToken -> m Unit
  , getToken :: forall m. MonadEffect m => Unit -> m (Maybe JwtToken)
  , eventSource :: forall m. MonadAff m => String -> EventSource m (Maybe JwtToken)
  }

init :: forall m. MonadAff m => Maybe JwtToken -> m Handle
init initToken = do
  current <- liftEffect $ Ref.new initToken
  -- Queue to broadcast new Token/Users
  queue <- liftEffect $ Queue.new
  let
    setToken :: forall m'. MonadEffect m' => Maybe JwtToken -> m' Unit
    setToken token = liftEffect do
      Ref.write token current
      Queue.broadcast queue token

    getToken :: forall m'. MonadEffect m' => Unit -> m' (Maybe JwtToken)
    getToken unit = liftEffect $ Ref.read current

    eventSource :: forall m'. MonadAff m' => String -> EventSource m' (Maybe JwtToken)
    eventSource subId =
      effectEventSource \emitter -> do
        Queue.on queue subId  (emit emitter)
        -- on unsubscribe remove the handler from the queue
        pure $ Finalizer (void $ Queue.del queue subId)
  pure
    $ Handle
        { setToken
        , getToken
        , eventSource
        }

subscribe :: forall m. MonadAff m => Handle -> String -> (Maybe JwtToken -> HookM m Unit) -> HookM m SubscriptionId
subscribe (Handle h) subId onTokenChanged = Hooks.subscribe (map onTokenChanged $ h.eventSource subId)

putToken :: forall m. MonadEffect m => Handle -> Maybe JwtToken -> m Unit
putToken (Handle h) token = h.setToken token

readToken :: forall m. MonadEffect m => Handle -> m (Maybe JwtToken)
readToken (Handle h) = h.getToken unit

now with halogen-subscriptions out I could remove the extra queue there and either that or the new version together with halogen-6 seems to have solved this issue (at least for me).