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: