Skip to content

Commit

Permalink
Add error-handling exercise version 1
Browse files Browse the repository at this point in the history
  • Loading branch information
ageron committed Sep 12, 2024
1 parent 7f764e3 commit 9d188d1
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 0 deletions.
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,14 @@
"prerequisites": [],
"difficulty": 5
},
{
"slug": "error-handling",
"name": "Error Handling",
"uuid": "a804dd5f-02cc-42e1-ab43-1f2c9ef0d0c4",
"practices": [],
"prerequisites": [],
"difficulty": 1
},
{
"slug": "flatten-array",
"name": "Flatten Array",
Expand Down
43 changes: 43 additions & 0 deletions exercises/practice/error-handling/.docs/instructions.append.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Instructions append

You are building a tiny web server that queries an even tinier user database. Sounds easy, but in the real world many things can (and often do) go wrong:

- the connection may be insecure: the URL must start with `"https://"`
- the domain name may be invalid: in this exercise, it should be `"example.com"`
- the page may not be found: only `"https://example.com/"`, `"https://example.com/users/"` and `"https://example.com/users/<userId>"` are allowed
- the `userId` may not be a positive integer
- no user may exist in the database with this `userId`

When things go wrong, it's important to give the end user a nice and helpful error message so that they can solve the issue. For this, you need to ensure that your code propagates informative errors from the point where the error is detected to the point where the error message is produced.

Luckily, Roc allows you to carry payload (i.e., data) inside your `Err` tag. It's tempting to carry an error message directly (e.g., `Err "User #42 was not found"`), and this may be fine in some simple cases, but this has several limitations:

- you might not have enough context inside the function that detects the error to produce a sufficiently helpful error message.
- For example, the `Str.toU64` function can be used to parse all sorts of integers: days, seconds, user IDs, and more. If the error message just says `Could not convert string "0.5" to a positive integer`, the user may not have enough context to solve the issue.
- in many cases your error handling code may need to handle some errors differently than others. However, if the errors only carry string payloads, your error handling code will have to parse that string to know what problem occurred: this is inefficient and it can easily break if the error message is ever tweaked.
- if your website is multilingual, you will need to translate the error message to the user's language. It's going to be much easier to do that if the error payload is machine-friendly data rather than an English string.

So in this exercise, your errors will instead carry a meaningful tag along with its own helpful payload. For example, if the user is not found, the error will look like `Err (UserNotFound 42)`.

Okay, let's get started! Here's what you need to do:

1. Implement `getUser` to return the requested user from the `users` "database" (it's actually just a `Dict`). Make sure the function returns `Err (UserNotFound userId)` in case the user is not found, instead of `Err KeyNotFound`.
2. Implement `parseUserId` to convert the URL's path (such as `"/users/123"`) to a positive integer user ID (`123`). In case of error, return `Err (InvalidUserId userIdStr)`.
3. Implement `getPage`:
- If the URL is `"https://example.com/"`, return `Ok "Home page"`
- If the URL is `"https://example.com/users/"`, return `Ok "Users page"`
- If the URL is `"https://example.com/users/<userId>"`, parse the user ID, load the user with that ID, and return `Ok "<user name>'s page"`
- If the URL prefix is not `"https://"`, return `Err (InsecureConnection url)`
- If the URL domain name is not `"example.com"`, return `Err (InvalidDomain url)`
- If the path is not `/` or `/users/` or `/users/<user id>`, return `Err (PageNotFound path)`
- If the user ID is not a positive integer, return `Err (InvalidUserId userIdStr)`
- If the user does not exist, return `Err (UserNotFound userId)`
4. Implement `errorMessage` to convert the previous errors to translated error messages. The function should at least handle English, but you are encouraged to try handling another language as well. The English error messages should like this:

- `"Insecure connection (non HTTPS): http://example.com/users/789"`
- `"Invalid domain name: https://google.com/wrong"`
- `"Page not found: /oops"`
- `"User ID is not a positive integer: abc"`
- `"User #42 was not found"`

Note: instead of displaying an error message to the user when they use HTTP instead of HTTPS, your web server could redirect their browser to HTTPS, and the user would not even notice the error. Since the errors are machine-friendly in Roc, this would be very easy to implement.
8 changes: 8 additions & 0 deletions exercises/practice/error-handling/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Instructions

Implement various kinds of error handling and resource management.

An important point of programming is how to handle errors and close resources even if errors occur.

This exercise requires you to handle various errors.
Because error handling is rather programming language specific you'll have to refer to the tests for your track to see what's exactly required.
61 changes: 61 additions & 0 deletions exercises/practice/error-handling/.meta/Example.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module [getUser, parseUserId, getPage, errorMessage]

User : { name : Str }

users : Dict U64 User
users =
Dict.fromList [
(123, { name: "Alice" }),
(456, { name: "Bob" }),
(789, { name: "Charlie" }),
]

getUser : U64 -> Result User [UserNotFound U64]
getUser = \userId ->
users |> Dict.get userId |> Result.mapErr \KeyNotFound -> UserNotFound userId

parseUserId : Str -> Result U64 [InvalidUserId Str]
parseUserId = \path ->
userIdStr = path |> Str.replaceFirst "/users/" ""
userIdStr |> Str.toU64 |> Result.mapErr \InvalidNumStr -> InvalidUserId userIdStr

parsePath : Str -> Result Str [InvalidDomain Str, InsecureConnection Str]
parsePath = \url ->
prefix = "https://example.com"
if url |> Str.startsWith prefix then
url |> Str.replaceFirst prefix "" |> Ok
else if url |> Str.startsWith "https://" then
Err (InvalidDomain url)
else
Err (InsecureConnection url)

getPage : Str -> Result Str [InsecureConnection Str, InvalidDomain Str, InvalidUserId Str, UserNotFound U64, PageNotFound Str]
getPage = \url ->
when parsePath? url is
"/" -> Ok "Home page"
"/users/" -> Ok "Users page"
userPath if userPath |> Str.startsWith "/users/" ->
userId = parseUserId? userPath
user = getUser? userId
Ok "$(user.name)'s page"

unknownPath -> Err (PageNotFound unknownPath)

errorMessage : [InsecureConnection Str, InvalidDomain Str, InvalidUserId Str, UserNotFound U64, PageNotFound Str], [English, French] -> Str
errorMessage = \err, language ->
when language is
English ->
when err is
InsecureConnection url -> "Insecure connection (non HTTPS): $(url)"
InvalidDomain url -> "Invalid domain name: $(url)"
InvalidUserId userIdStr -> "User ID is not a positive integer: $(userIdStr)"
UserNotFound userId -> "User #$(userId |> Num.toStr) was not found"
PageNotFound path -> "Page not found: $(path)"

French ->
when err is
InsecureConnection url -> "Connexion non sécurisée (non HTTPS): $(url)"
InvalidDomain url -> "Ce nom de domaine est incorrect: $(url)"
InvalidUserId userIdStr -> "Cet identifiant utilisateur devrait être un entier positif: $(userIdStr)"
UserNotFound userId -> "L'utilisateur #$(userId |> Num.toStr) est inconnu"
PageNotFound path -> "Cette page est inconnue: $(path)"
17 changes: 17 additions & 0 deletions exercises/practice/error-handling/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"authors": [
"ageron"
],
"files": {
"solution": [
"ErrorHandling.roc"
],
"test": [
"error-handling-test.roc"
],
"example": [
".meta/Example.roc"
]
},
"blurb": "Implement various kinds of error handling and resource management."
}
27 changes: 27 additions & 0 deletions exercises/practice/error-handling/ErrorHandling.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module [getUser, parseUserId, getPage, errorMessage]

User : { name : Str }

users : Dict U64 User
users =
Dict.fromList [
(123, { name: "Alice" }),
(456, { name: "Bob" }),
(789, { name: "Charlie" }),
]

getUser : U64 -> Result User [UserNotFound Str]
getUser = \userId ->
crash "Please implement the 'getUser' function"

parseUserId : Str -> Result U64 [InvalidUserId Str]
parseUserId = \path ->
crash "Please implement the 'parseUserId' function"

getPage : Str -> Result Str _
getPage = \url ->
crash "Please implement the 'getPage' function"

errorMessage : _, [English] -> Str
errorMessage = \err, language ->
crash "Please implement the 'errorMessage' function"
197 changes: 197 additions & 0 deletions exercises/practice/error-handling/error-handling-test.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# File last updated on 2024-09-12
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br",
}

main =
Task.ok {}

import ErrorHandling exposing [getUser, parseUserId, getPage, errorMessage]

##
## getUser should return Ok <user> or Err UserNotFound <userId>
##

# getUser 123 should return Alice
expect
result = getUser 123
result == Ok { name: "Alice" }

# getUser 456 should return Bob
expect
result = getUser 456
result == Ok { name: "Bob" }

# getUser 789 should return Charlie
expect
result = getUser 789
result == Ok { name: "Charlie" }

# getUser 42 should return an error
expect
result = getUser 42
result == Err (UserNotFound 42)

##
## parseUserId should parse a string to a positive integer and return
## Ok <userId> if successful, or Err InvalidUserId <userIdStr> otherwise
##

# Parsing a valid userId should return Ok <userId>
expect
result = parseUserId "123"
result == Ok 123

# Parsing an empty string should fail
expect
result = parseUserId ""
result == Err (InvalidUserId "")

# Parsing a negative number should fail
expect
result = parseUserId "-123"
result == Err (InvalidUserId "-123")

# Parsing a fractional number should fail
expect
result = parseUserId "123.456"
result == Err (InvalidUserId "123.456")

# Parsing a number in scientific format should fail
expect
result = parseUserId "1e03"
result == Err (InvalidUserId "1e03")

# Parsing a string containing letters should fail
expect
result = parseUserId "abc"
result == Err (InvalidUserId "abc")

# Parsing a string containing a valid userId followed by junk should fail
expect
result = parseUserId "123 abc"
result == Err (InvalidUserId "123 abc")

##
## getPage should return Ok with the desired page if it exists
## or it should return the proper Err value if anything fails
##

# No error for root URL
expect
result = getPage "https://example.com/"
result == Ok "Home page"

# No error for users URL
expect
result = getPage "https://example.com/users/"
result == Ok "Users page"

# No error for specific user URL
expect
result = getPage "https://example.com/users/123"
result == Ok "Alice's page"

# No error for specific user URL
expect
result = getPage "https://example.com/users/456"
result == Ok "Bob's page"

# No error for specific user URL
expect
result = getPage "https://example.com/users/789"
result == Ok "Charlie's page"

# Error: insecure connection
expect
result = getPage "http://example.com/users/789"
result == Err (InsecureConnection "http://example.com/users/789")

# Error: invalid domain name
expect
result = getPage "https://google.com/wrong"
result == Err (InvalidDomain "https://google.com/wrong")

# Error: page not found
expect
result = getPage "https://example.com/oops"
result == Err (PageNotFound "/oops")

# Error: invalid userId
expect
result = getPage "https://example.com/users/abc"
result == Err (InvalidUserId "abc")

# Error: user not found
expect
result = getPage "https://example.com/users/42"
result == Err (UserNotFound 42)

##
## Handle errors and return a clear message to the user, in the user's language
## Your implementation must at least handle English, but you can handle other
## languages if you want
##

# No error for root URL: just return the Ok result
expect
pageResult = getPage "https://example.com/"
result = pageResult |> Result.mapErr \err -> err |> errorMessage English
result == Ok "Home page"

# No error for users URL: just return the Ok result
expect
pageResult = getPage "https://example.com/users/"
result = pageResult |> Result.mapErr \err -> err |> errorMessage English
result == Ok "Users page"

# No error for specific user URL: just return the Ok result
expect
pageResult = getPage "https://example.com/users/123"
result = pageResult |> Result.mapErr \err -> err |> errorMessage English
result == Ok "Alice's page"

# No error for specific user URL: just return the Ok result
expect
pageResult = getPage "https://example.com/users/456"
result = pageResult |> Result.mapErr \err -> err |> errorMessage English
result == Ok "Bob's page"

# No error for specific user URL: just return the Ok result
expect
pageResult = getPage "https://example.com/users/789"
result = pageResult |> Result.mapErr \err -> err |> errorMessage English
result == Ok "Charlie's page"

# Error: insecure connection
# Note: instead of displaying an error message, the server could automatically
# redirect the user to the HTTPS URL. This is an example of a recoverable error
# which would be easy to handle because the error payload is machine-friendly
expect
pageResult = getPage "http://example.com/users/789"
result = pageResult |> Result.mapErr \err -> err |> errorMessage English
result == Err "Insecure connection (non HTTPS): http://example.com/users/789"

# Error: invalid domain name
expect
pageResult = getPage "https://google.com/wrong"
result = pageResult |> Result.mapErr \err -> err |> errorMessage English
result == Err "Invalid domain name: https://google.com/wrong"

# Error: page not found
expect
pageResult = getPage "https://example.com/oops"
result = pageResult |> Result.mapErr \err -> err |> errorMessage English
result == Err "Page not found: /oops"

# Error: invalid userId
expect
pageResult = getPage "https://example.com/users/abc"
result = pageResult |> Result.mapErr \err -> err |> errorMessage English
result == Err "User ID is not a positive integer: abc"

# Error: user not found
expect
pageResult = getPage "https://example.com/users/42"
result = pageResult |> Result.mapErr \err -> err |> errorMessage English
result == Err "User #42 was not found"

0 comments on commit 9d188d1

Please sign in to comment.