Tasks & Error Handling

This example shows how to use Task with the basic-cli platform. We'll explain how tasks work while demonstrating how to read command line arguments and environment variables, write files, and fetch content through HTTP.

We recommend you read the tasks and backpassings sections in the tutorial first and open up the documentation for the basic-cli platform on the side.

Remember; a Task represents an effect; an interaction with state outside your Roc program, such as the terminal's standard output, or a file.

Below we'll introduce the example code step by step, you can check out the full code at any time at the bottom.

main

The roc-lang/basic-cli platform requires an application to provide a Task, namely main : Task {} *. This task usually represents a sequence or combination of Tasks, and will resolve to an empty record {}. This is similar to void or unit in other programming languages.

The main task is run by the platform when the application is executed. It cannot return errors, which is indicated by the *.

For this example, we'll be using the following main:

main : Task {} *
main =
    run
    |> Task.onErr handleErr

# Error : [ FailedToReadArgs, FailedToFetchHtml Str, ... ]

handleErr : Error -> Task {} *

run : Task {} Error

The run : Task {} Error task resolves to a success value of an empty record, and if it fails, returns with our custom Error type.

This simplifies error handling so that a single handleErr function can be used to handle all the Error values that could occur.

run

We want to see how fast our app runs, so we'll start our run Task by getting the current time.

startTime = Utc.now!

To get the current time, we need to interact with state outside of the roc program. We can not just calculate the current time, so we use a task, Utc.now. It's type is Task Utc *. The task resolves to the UTC time (since Epoch).

Read an environment variable

Next up in the task chain we'll read the environment variable HELLO:

helloEnvVar = readEnvVar! "HELLO"

# …

readEnvVar : Str -> Task Str *

And print it (to stdout):

Stdout.line! "HELLO env var was set to $(helloEnvVar)"

Command line arguments

When reading command line arguments, it's nice to be able to read multiple arguments. We can use record destructuring to fit these multiple arguments nicely in our chain:

{ url, outputPath } = readArgs!

# …

readArgs : Task { url: Str, outputPath: Path } [FailedToReadArgs]_

Notice that readArgs can actually return an error unlike the previous tasks, namely FailedToReadArgs. By using ! syntax to chain our tasks we can deal with errors at the end so it doesn't interrupt the flow of our code right now.

The underscore (_) at the end of [FailedToReadArgs] is a temporary workaround for an issue.

Note: running the formatter on the readArgs implementation currently results in a parser issue, so skip formatting as a temporary workaround until it's fixed.

Fetch website content

We'll use the url we obtained in the previous step and retrieve its contents:

strHTML = fetchHtml! url

Write to a file

Next up, we'll write our strHTML to a file located at outputPath.

File.writeUtf8 outputPath strHTML
|> Task.onErr! \_ -> Task.err (FailedToWriteFile outputPath)

The File.writeUtf8 task resolves to an empty record if the provided Str is sucessfully written. The error type for writeUtf8 is [FileWriteErr Path WriteErr] but we'd like to replace it with our own simpler error here. For that we use Task.onErr.

List the contents of a directory

We're going to finish up with something more involved:

listCwdContent
|> Task.map \dirContents ->
    List.map dirContents Path.display
    |> Str.joinWith ","

|> Task.await! \contentsStr ->
    Stdout.line "Contents of current directory: $(contentsStr)"

# …

listCwdContent : Task (List Path) [FailedToListCwd]_

We call listCwdContent to list all files and folders in the current directory. Next, we take this list of paths, turn them all into Str using Path.display, and join/concatenate this list with a ",".

We use Task.map to transform the success value of a Task into something that is not a Task, a Str in this case.

Take a minute to look at the similarities and differences of Task.map and Task.await:

Task.map : Task a b, (a -> c) -> Task c b

Task.await : Task a b, (a -> Task c b) -> Task c b

Next, we write our Str of combined dirContents to Stdout. We use Task.await because we're passing it a function that returns a Task with Stdout.line.

Feedback

Tasks are important in roc, we'd love to hear how we can further improve this example. Get in touch on our group chat or create an issue.

Full Code

app [main] {
    pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.17.0/lZFLstMUCUvd5bjnnpYromZJXkQUrdhbva4xdBInicE.tar.br",
}

import pf.Stdout
import pf.Stderr
import pf.Arg
import pf.Env
import pf.Http
import pf.Dir
import pf.Utc
import pf.Path exposing [Path]

main : Task {} _
main = run |> Task.onErr handleErr

run : Task {} _
run =

    # Get time since [Unix Epoch](https://en.wikipedia.org/wiki/Unix_time)
    startTime = Utc.now! {}

    # Read the HELLO environment variable
    helloEnvVar =
        readEnvVar "HELLO"
            |> Task.map! \msg -> if Str.isEmpty msg then "was empty" else "was set to $(msg)"
    Stdout.line! "HELLO env var $(helloEnvVar)"

    # Read command line arguments
    { url, outputPath } = readArgs!
    Stdout.line! "Fetching content from $(url)..."

    # Fetch the provided url using HTTP
    strHTML = fetchHtml! url
    Stdout.line! "Saving url HTML to $(Path.display outputPath)..."
    # Write HTML string to a file
    Path.writeUtf8 strHTML outputPath 
        |> Task.onErr! \_ -> Task.err (FailedToWriteFile outputPath)
    # Print contents of current working directory
    listCwdContent
        |> Task.map \dirContents ->
            List.map dirContents Path.display
            |> Str.joinWith ","
        |> Task.await! \contentsStr ->
            Stdout.line "Contents of current directory: $(contentsStr)"

    endTime = Utc.now! {}
    runTime = Utc.deltaAsMillis startTime endTime |> Num.toStr
    Stdout.line! "Run time: $(runTime) ms"
    # Final task doesn't need to be awaited
    Stdout.line! "Done"

# NOTE in the future the trailing underscore `_` character will not be necessary.
# This is a temporary workaround until [this issue](https://github.com/roc-lang/roc/issues/5660)
# is resolved.

readArgs : Task { url : Str, outputPath : Path } [FailedToReadArgs]_
readArgs =
    when Arg.list! {} is
        [_, first, second, ..] ->
            Task.ok { url: first, outputPath: Path.fromStr second }

        _ ->
            Task.err FailedToReadArgs

readEnvVar : Str -> Task Str []_
readEnvVar = \envVarName ->
    when Env.var envVarName |> Task.result! is
        Ok envVarStr if !(Str.isEmpty envVarStr) ->
            Task.ok envVarStr

        _ ->
            Task.ok ""

fetchHtml : Str -> Task Str [FailedToFetchHtml _]_
fetchHtml = \url ->
    { Http.defaultRequest & url }
    |> Http.send
    |> Task.await \resp -> resp |> Http.handleStringResponse |> Task.fromResult
    |> Task.mapErr FailedToFetchHtml

listCwdContent : Task (List Path) [FailedToListCwd]_
listCwdContent =
    Dir.list "."
    |> Task.onErr \_ -> Task.err FailedToListCwd

handleErr : _ -> Task {} _
handleErr = \err ->
    usage = "HELLO=1 roc main.roc -- \"https://www.roc-lang.org\" roc.html"

    errorMsg =
        when err is
            FailedToReadArgs -> "Failed to read command line arguments, usage: $(usage)"
            FailedToFetchHtml httpErr -> "Failed to fetch URL $(Inspect.toStr httpErr), usage: $(usage)"
            FailedToWriteFile path -> "Failed to write to file $(Path.display path), usage: $(usage)"
            FailedToListCwd -> "Failed to list contents of current directory, usage: $(usage)"
            _ -> Inspect.toStr err
    Stderr.line! "Error: $(errorMsg)"

Output

Run this from the directory that has main.roc in it:

$ HELLO=1 roc examples/Tasks/main.roc -- "https://www.roc-lang.org" roc.html
HELLO env var was set to 1
Fetching content from https://www.roc-lang.org...
Saving url HTML to roc.html...
Contents of current directory: [...]
Run time: 329 ms
Done