Handling unreachable cases in ports

Hi, sorry if this is a frequently asked question, but I’ve been digging around for hours and have found only bits of information, with no clear answer - also very much my first rodeo with Elm, so bear with me :wink:

In a nutshell, I have this case:

type PlayerUpdate
    = Playing


decodePlayerUpdate : D.Decoder PlayerUpdate
decodePlayerUpdate =
    D.field "update" D.string
        |> D.andThen decodePlayerUpdateType


decodePlayerUpdateType : String -> D.Decoder PlayerUpdate
decodePlayerUpdateType kind =
    case kind of
        "playing" ->
            D.succeed Playing

        _ ->
            D.fail "invalid update type"


port playerUpdateReceiver : (E.Value -> msg) -> Sub msg
-- + Some code for hooking this up to a subscription, which uses the above decodePlayerUpdate

My API surface currently only permits “playing”, of course - if anything else is sent, this is a failure. I’d like my application to crash and burn if this happens, because it means something has gone horribly wrong on the JS side.

Now, of course I need to handle all result cases, and this is excellent, but the only way to handle this failure case is to bubble this all the way up to the model. For the moment, I just return my model unchanged, ignoring the error. This is obviously bad.

I think I have two options:

  • Reflect the failure in my view somehow, adding a user-visible failure message, and add a state in which any update doesn’t change the model anymore.
    • Cute, but this means I can’t check for it in programmatic testing too easily, and I don’t actually want to expose this to a user - ideally this traces to some kind of analytics tool instead.
    • Debug.log doesn’t help, sadly, at least according to the documentation it’s optimized away.
  • Send a debug message to JS so that it can print to the console, and mess with the elm runtime enough that it stops moving.
    • JS is misbehaving, so I can’t really trust this message to be delivered. This might even cause a recursive failure, where each message triggers another.
    • I’d rather not mess with elm internals from within JS, this is a horrible idea and I feel bad for even having it.

Obviously, neither of these options is particularly appealing. Worse yet, they come with a fair bit of additional type management, and make the json decoding and model management code more complex than it needs to be, for data that I ultimately ignore in a case that should never happen (and I can be reasonably sure will never happen by synchronizing my typescript and elm models of this internal API).

In e.g. Rust, I would mark this with a .expect() a bit further down the line so that I can get a nice way to trace the error if this does ever happen in production. As best as I can tell, a Debug.crash used to exist before elm 0.19 and was used for these kinds of cases, but has since been removed.

Am I simply falling afoul of an ideological pursuit of always modelling the whole application state? Is there some way to poke a hole in here and actually get a stack trace when I want one? Or am I modelling my ports entirely wrong and there is a nicer way to get variants out of my port?

2 Likes

Personally I’d not bother with trying to circumvent the runtime.
I’d add an “error”-page from which you cannot navigate easily and then on this kind of error have the app navigate to it.
If you are not on a “application” with navigation you can always have a catastrophicalError : Maybe something field in your model and in view/update a top-level if/case that’ll stop updating and show some kind of error.

Other options:

  • use a port and have Javascript navigate to a blank/other page
  • have a recursive function calling itself (infinite loop - not recommended - browsers don’t like that too much)

Elm prides itself to never crash, even if it goes against the developer’s wishes - after all, who knows best?
Your best bet is to create a view for the failing case, maybe send a message to JS to somehow save a dump of the error. If you really want to grind the runtime to a halt you can always divide by zero, but I don’t think that would be really helpful.

Hrm, it’s still pretty easy to crash Elm, if you use e.g.

port playerUpdateReceiver : (String -> msg) -> Sub msg

and send the wrong type - which is the exact same type of error that I’d like to make crash, mind you.

But fair enough, I’ll take it as a thing elm doesn’t like to do. Is there an idiomatic way of actually making this transparent to debuggers without calling out to JavaScript? The only situation in which errors like this can happen are when I can’t trust my ports, so having to call out to ports seems unfortunate.

Ultimately whatever strategy chosen will still be JS at the end of the day since any Elm code written will be compiled down to JS.

I’ve found it’s fine to just isolate the error-handling paths from the rest of the JS code. Usually I’ll use a separate dedicated port for errors and hand-write 10-20 lines of JS that don’t depend on any 3rd or 1st party libraries. (Using XMLHttpRequest or fetch directly.) This way whatever bugs might exist in the normal port handling JS can’t influence the error-logging path at all.

Alternatively, you can use the elm/http library to send the error directly from Elm without touching JS yourself. It’s still JS under-the-hood, just JS written by Evan instead of you. I think it would ultimately be better to write the JS yourself in this case. Since it’s so much smaller than elm/http, and a part of your project, it will be much easier to audit manually for bugs.

As for getting a stack trace, you could get one in the error port JS like this. But since the Elm runtime is async, you’re mostly going to end up with stack frames from Elm’s async scheduler which doesn’t seem terribly useful. Personally, I haven’t found the lack of stack-traces to be an issue when debugging production Elm code, but that may just indicate that I’ve never had a particularly nasty bug.

Here are a few alternatives to stack-traces:

  • Keep a list of msgs in the model. When sending an error message down the port, include the last n msgs that your application handled.
  • Serialize the entire model or parts of the model and send that over the port with the error message. If done correctly you’ll be able to restore the entire application state for debugging.
  • If the error is specifically about decoding like in your example, include the value being decoded and the error given by the decoder. That should be enough info to pinpoint where and why the decoding failed.
2 Likes

Yep, this seems like the only possible solution to tick most of my boxes. I don’t think the http library is sufficient, because it again doesn’t help much with automated tests.

I’m a bit saddened by it, because I specifically don’t trust the ports anymore once things go this wrong - the only way this can happen is if, say, the site was deployed with the wrong underlying implementation on the JS side for the ports, or something else outlandish like that (maybe a browser extension messing with things it should not).

But I suppose at that point I’m really optimizing for something that will never happen in practice. Ignoring the errors and using Debug.log whenever one turns up will probably be enough. It’s a shame I can’t model this situation correctly, but well, at least I know about this limitation - and it’s still significantly nicer than the react alternative.

1 Like

Actually, x / 0 is NaN, Infinity or -Infinity and x // 0 == 0. Using modBy 0 0 does work (although with the given definition of //, modBy 0 x == x would be more sensible) but it does not help with getting the types to line up.

You can model a “halted” state for your program with something like this:

type Model
  = Running ProgramState
  | Halted FailureInfo

This, and the code changes they would prompt you to make, would ensure that your program can’t continue running normally if a fatal problem occurs – you can decide the exact behavior you want for your program’s “halted” state.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.