Results & Error Handling

TODO update this example with a snippet using the try keyword, see issue #227

This example shows how to use Result in functions that can return errors. We will see how to use Result.try or the try operator ? to chain functions and return the first error if any occurs.

Code

app [main!] {
    pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.18.0/0APbwVN1_p1mJ96tXjaoiUCr8NBGamr8G8Ac_DrXR-o.tar.br",
}

import pf.Stdout

## This function parses strings like "{FirstName} {LastName} was born in {Year}"
## and if successful returns `Ok {firstName, lastName, birthYear}`. Otherwise
## it returns an `Err` containing a descriptive tag.
## This is the most verbose version, we will do better below.
parse_verbose = \line ->
    when line |> Str.splitFirst " was born in " is
        Ok { before: full_name, after: birth_year_str } ->
            when full_name |> Str.splitFirst " " is
                Ok { before: first_name, after: last_name } ->
                    when Str.toU16 birth_year_str is
                        Ok birth_year ->
                            Ok { first_name, last_name, birth_year }

                        Err _ -> Err InvalidBirthYearFormat

                _ -> Err InvalidNameFormat

        _ -> Err InvalidRecordFormat

## Here's a very slightly shorter version using `Result.try` to chain multiple
## functions that each could return an error. It's a bit nicer, don't you think?
## Note: this version returns "raw" errors (`Err NotFound` or `Err InvalidNumStr`).
parse_with_try = \line ->
    line
    |> Str.splitFirst " was born in "
    |> Result.try \{ before: full_name, after: birth_year_str } ->
        full_name
        |> Str.splitFirst " "
        |> Result.try \{ before: first_name, after: last_name } ->
            Str.toU16 birth_year_str
            |> Result.try \birth_year ->
                Ok { first_name, last_name, birth_year }

## This version is like `parseWithTry`, except it uses `Result.mapErr`
## to return more informative errors, just like the ones in `parseVerbose`.
parse_with_try_v2 = \line ->
    line
    |> Str.splitFirst " was born in "
    |> Result.mapErr \_ -> Err InvalidRecordFormat
    |> Result.try \{ before: full_name, after: birth_year_str } ->
        full_name
        |> Str.splitFirst " "
        |> Result.mapErr \_ -> Err InvalidNameFormat
        |> Result.try \{ before: first_name, after: last_name } ->
            Str.toU16 birth_year_str
            |> Result.mapErr \_ -> Err InvalidBirthYearFormat
            |> Result.try \birth_year ->
                Ok { first_name, last_name, birth_year }

## The `?` operator, called the "try operator", is
## [syntactic sugar](en.wikipedia.org/wiki/Syntactic_sugar) for `Result.try`.
## It makes the code much less nested and easier to read.
## The following function is equivalent to `parseWithTry`:
parse_with_try_op = \line ->
    { before: full_name, after: birth_year_str } = Str.splitFirst? line " was born in "
    { before: first_name, after: last_name } = Str.splitFirst? full_name " "
    birth_year = Str.toU16? birth_year_str
    Ok { first_name, last_name, birth_year }

## And lastly the following function is equivalent to `parseWithTryV2`.
## Note that the `?` operator has moved from `splitFirst` & `toU16` to `mapErr`:
parse_with_try_op_v2 = \line ->
    { before: full_name, after: birth_year_str } =
        line
        |> Str.splitFirst " was born in "
        |> Result.mapErr? \_ -> Err InvalidRecordFormat
    { before: first_name, after: last_name } =
        full_name
        |> Str.splitFirst " "
        |> Result.mapErr? \_ -> Err InvalidNameFormat
    birth_year =
        Str.toU16 birth_year_str
        |> Result.mapErr? \_ -> Err InvalidBirthYearFormat
    Ok { first_name, last_name, birth_year }

## This function parses a string using a given parser and returns a string to
## display to the user. Note how we can handle errors individually or in bulk.
parse = \line, parser ->
    when parser line is
        Ok { first_name, last_name, birth_year } ->
            """
            Name: $(last_name), $(first_name)
            Born:  $(birth_year |> Num.toStr)

            """

        Err InvalidNameFormat -> "What kind of a name is this?"
        Err InvalidBirthYearFormat -> "That birth year looks fishy."
        Err InvalidRecordFormat -> "Oh wow, that's a weird looking record!"
        _ -> "Something unexpected happened" # Err NotFound or Err InvalidNumStr

main! = \_args ->
    try Stdout.line! (parse "George Harrison was born in 1943" parse_verbose)
    try Stdout.line! (parse "John Lennon was born in 1940" parse_with_try)
    try Stdout.line! (parse "Paul McCartney was born in 1942" parse_with_try_v2)
    try Stdout.line! (parse "Ringo Starr was born in 1940" parse_with_try_op)
    try Stdout.line! (parse "Stuart Sutcliffe was born in 1940" parse_with_try_op_v2)

    Ok {}

Output

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

roc examples/Results/main.roc