Results & Error Handling

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.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.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.
parseVerbose = \line ->
    when line |> Str.splitFirst " was born in " is
        Ok { before: fullName, after: birthYearStr } ->
            when fullName |> Str.splitFirst " " is
                Ok { before: firstName, after: lastName } ->
                    when Str.toU16 birthYearStr is
                        Ok birthYear ->
                            Ok { firstName, lastName, birthYear }

                        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`).
parseWithTry = \line ->
    line
    |> Str.splitFirst " was born in "
    |> Result.try \{ before: fullName, after: birthYearStr } ->
        fullName
        |> Str.splitFirst " "
        |> Result.try \{ before: firstName, after: lastName } ->
            Str.toU16 birthYearStr
            |> Result.try \birthYear ->
                Ok { firstName, lastName, birthYear }

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

## 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`:
parseWithTryOp = \line ->
    { before: fullName, after: birthYearStr } = Str.splitFirst? line " was born in "
    { before: firstName, after: lastName } = Str.splitFirst? fullName " "
    birthYear = Str.toU16? birthYearStr
    Ok { firstName, lastName, birthYear }

## And lastly the following function is equivalent to `parseWithTryV2`.
## Note that the `?` operator has moved from `splitFirst` & `toU16` to `mapErr`:
parseWithTryOpV2 = \line ->
    { before: fullName, after: birthYearStr } =
        line
            |> Str.splitFirst " was born in "
            |> Result.mapErr? \_ -> Err InvalidRecordFormat
    { before: firstName, after: lastName } =
        fullName
            |> Str.splitFirst " "
            |> Result.mapErr? \_ -> Err InvalidNameFormat
    birthYear =
        Str.toU16 birthYearStr
            |> Result.mapErr? \_ -> Err InvalidBirthYearFormat
    Ok { firstName, lastName, birthYear }

## 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 { firstName, lastName, birthYear } ->
            """
            Name: $(lastName), $(firstName)
            Born:  $(birthYear |> 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 =
    "George Harrison was born in 1943" |> parse parseVerbose |> Stdout.line!
    "John Lennon was born in 1940" |> parse parseWithTry |> Stdout.line!
    "Paul McCartney was born in 1942" |> parse parseWithTryV2 |> Stdout.line!
    "Ringo Starr was born in 1940" |> parse parseWithTryOp |> Stdout.line!
    "Stuart Sutcliffe was born in 1940" |> parse parseWithTryOpV2 |> Stdout.line!

Output

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

roc examples/Results/main.roc