Example of a fallback Task with andThen and onError (add to documentation?)

I recently started my first Elm project, and I’m yet to fully wrap my head around Tasks, and specifically, how to set off another Task if the previous one fails. I finally managed to get it working after some trial and error, so I’m sharing it here case someone else (or my future self) finds it useful!

(I broke it down in detail but if you’re an experienced Elm developer you can probably just skip to the “My solution” section :wink:)

The problem

Sometimes, I need to know where in the page a user has scrolled. I can do this using Browser.Dom.getViewport like this:

type Msg
    = GotViewport Browser.Dom.Viewport

getViewport : Cmd Msg
getViewport =
    Task.perform GotViewport Browser.Dom.getViewport

However, most of the time I’m not scrolling the body directly, but rather a div that’s the size of a screen. That means I need to use Browser.Dom.getViewportOf instead:

type Msg
    = GotViewport
    | GotDivViewport (Result Browser.Dom.Error Browser.Dom.Viewport)

getDivViewport : Cmd Msg
getDivViewport =
    Task.attempt GotDivViewport (Browser.Dom.getViewportOf "terrain")

-- (also getViewport code as before)

…and then keep juggling both functions to call whichever is appropriate. That includes logic to match getDivViewport against Ok and Error values, and in the latter case to run the getViewport function (which will later be processed).

To avoid all this, I was searching for a way to have just one getViewport function that tries the div-specificgetViewportOf "terrain" first, and falls back to the full-body getViewport if that fails.

My solution

It took me a while to get it working, and I’m still new enough to types that this final form was more guesswork and following Elm’s hints rather than independently working out how this is to be done;

type Msg
    = GotViewport Browser.Dom.Viewport

getViewport : Cmd Msg
getViewport =
    Browser.Dom.getViewportOf "terrain"
        |> Task.onError (\_ -> Browser.Dom.getViewport)
        |> Task.andThen (\result -> Task.succeed result)
        |> Task.perform GotViewport

The way I understand this is that onError runs the task, and, if it fails, returns a new task (which in this case is guaranteed to succeed). On the other hand, if the task succeeds, andThen returns a dummy task that is again guaranteed to succeed. Finally, Task.perform collects the newly generated task (real or dummy) and processes it, sending the result via GotViewport.

Update: just realised as I was typing this that the andThen line is superfluous; the code still works even if I remove it!

Clarifications

Am I right in thinking that andThen and onError don’t actually “run” the tasks but merely modify them?

Also, does this code (with the andThen line removed) look fine, or did I do something weird?

I was a bit confused because neither the andThen nor onError docs mentioned Task.attempt or Task.perform. Now that I look at it, I notice the examples’ return types are tasks too, so that explains why. Perhaps a more complete example in the docs will help? I don’t mind raising a PR if someone can give me feedback… :slightly_smiling_face:

1 Like

Just realised that onError is also automatically converting the return value from a Result to just the value! I think part of what got me stuck was assuming I’d have to do that myself :sweat_smile:

Would it be accurate to say that onError is the opposite of andThen?

1 Like

Assuming you have already worked with Javascript,
this is similar to try/catch in synchronous code or .then/.catch in asynchronous code.

You have two possible outputs, the “successful” case if you will and the “error” case which have different payloads.
In your example, the successful payload is the Viewport data type, and the error is an error object in case the “terrain” Html Element is not found.

Elm forces you to handle this via the type system since
the Task has returned by getViewportOf has a concrete error type. So you get to choose whether you want to handle this error in your update function by adjusting your corresponding message type or, as you did here, transform the error branch into something matching the success branch.

The reason why you can use Task.perform instead of attempt in the end is that getViewport cannot actually fail.
The type signature reflects that, since the Error type in its return type is generic i.e. it cannot happen.

In case you are wondering, it is still a task and not a normal function, since its output is non-deterministic. Calling it twice might lead to different outputs if the user resizes the window for example.

Hope this helps more than it confuses you :slightly_smiling_face:

1 Like

Thanks for the detailed breakdown! I was thinking along similar lines but it helps to get confirmation :slightly_smiling_face:

I’ve used Lisps before and played with OCaml a bit, so that helped me work things out too.

I didn’t really think of it, but the question of what constitutes a task was there at the back of my mind. This makes sense!

2 Likes

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