Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add errors concept #686

Merged
merged 8 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions concepts/errors/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"blurb": "Crystal has errors which can be raised and rescued. It is also possible to deffine your own exceptions.",
"authors": ["meatball133"],
"contributors": ["ryanplusplus"]
}
1 change: 1 addition & 0 deletions concepts/errors/.meta/template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% $errors %}
134 changes: 134 additions & 0 deletions concepts/errors/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Exception

In the ideal world, everything works perfectly.
But in the real world, things can go wrong, and how we handle these situations matters to ensure that our software is robust and reliable.
Exceptions are a crucial concept in programming that allows us to handle errors and unexpected situations gracefully.

Raising an error, if not handled, halts the program and throws an error message.
In most cases, you don't want your program to halt when an error occurs, instead you want to handle the error and continue running the program.

## Raising an exception

In Crystal, exceptions are raised using the `raise` keyword and can either be given a `String` or an `Exception` object.
If unhandled, the program will halt and print the error message.

```crystal
raise "This is an error"
```

There are several built-in exceptions in Crystal, like `ArgumentError`, `IndexError`, `KeyError`, `IOError`, `SystemCallError`, `TypeError`, `ZeroDivisionError` and many more.
These require you to pass a message to the exception.

```crystal
raise ArgumentError.new("This is an argument error")
```

## [Handling exceptions][[exception-handling]]

We wouldn't want our program to crash when an exception is raised.
Therefore, when we know a piece of code is error prone, we can wrap it in a `begin` block and rescue the exception with a `rescue` block.
The `begin` block marks the beginning of the code that might raise an exception, and the `rescue` block handles the exception.

```crystal
begin
raise "This is an error"
rescue
puts "An error occurred!"
end
```

The `rescue` block can also be specified with a variable to get the exception object.

```crystal
begin
raise "This is an error"
rescue ex
puts "An error occurred: #{ex.message}"
end
```

The `rescue` block can also be specified with a specific exception type only to catch that exception.

```crystal
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
end

# or

begin
raise ArgumentError.new("This is an argument error")
rescue ex : ArgumentError
puts "An argument error occurred: #{ex.message}"
end
```

Multiple `rescue` blocks can be used to handle different types of exceptions.
In the example below, the first `rescue` block will catch an `ArgumentError`, and the second `rescue` block will catch any other exception.

```crystal
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
rescue
puts "An error occurred!"
end
```

The `begin` block can also have an `else` block, which is executed if no exception is raised.

```crystal
begin
puts "No error occurred"
rescue
puts "An error occurred!"
else
puts "No error occurred"
end
```

Lastly, there is an `ensure` block that is always executed, regardless of whether an exception was raised.

```crystal
begin
raise "This is an error"
rescue
puts "An error occurred!"
ensure
puts "This is always executed"
end
```

## Method convention

Some methods have two versions, one with `!` and the other without.
This can mean two different things.
One is that the method mutates the object, and the other is that the method can raise an exception.

But there is also another convention around ending a method with `?` mentioned in the boolean concept.
Some methods raise an exception by default but also have a version ending with `?` which returns `nil` instead of raising an exception.

This is ideal when you want to avoid an error being raised.
This can benefit performance since it doesn't have to create a stack trace and, if set up correctly, could make the code safer.

## Custom exceptions

You can also create your own exceptions by inheriting from the [`Exception`][exception] class.
In doing so, you can optionally override the `initialize` method to set the exception message.
This can be done by assigning an instance variable named `@message` with the message.

```crystal
class MyException < Exception
def initialize
@message = "This is my exception"
end
end

raise MyException.new
```

[exception-handling]: https://crystal-lang.org/reference/syntax_and_semantics/exception_handling.html
[exception]: https://crystal-lang.org/api/Exception.html
134 changes: 134 additions & 0 deletions concepts/errors/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Exception

In the ideal world, everything works perfectly.
But in the real world, things can go wrong, and how we handle these situations matters to ensure that our software is robust and reliable.
Exceptions are a crucial concept in programming that allows us to handle errors and unexpected situations gracefully.

Raising an error, if not handled, halts the program and throws an error message.
In most cases, you don't want your program to halt when an error occurs, instead you want to handle the error and continue running the program.

## Raising an exception

In Crystal, exceptions are raised using the `raise` keyword and can either be given a `String` or an `Exception` object.
If unhandled, the program will halt and print the error message.

```crystal
raise "This is an error"
```

There are several built-in exceptions in Crystal, like `ArgumentError`, `IndexError`, `KeyError`, `IOError`, `SystemCallError`, `TypeError`, `ZeroDivisionError` and many more.
These require you to pass a message to the exception.

```crystal
raise ArgumentError.new("This is an argument error")
```

## [Handling exceptions][[exception-handling]]

We wouldn't want our program to crash when an exception is raised.
Therefore, when we know a piece of code is error prone, we can wrap it in a `begin` block and rescue the exception with a `rescue` block.
The `begin` block marks the beginning of the code that might raise an exception, and the `rescue` block handles the exception.

```crystal
begin
raise "This is an error"
rescue
puts "An error occurred!"
end
```

The `rescue` block can also be specified with a variable to get the exception object.

```crystal
begin
raise "This is an error"
rescue ex
puts "An error occurred: #{ex.message}"
end
```

The `rescue` block can also be specified with a specific exception type only to catch that exception.

```crystal
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
end

# or

begin
raise ArgumentError.new("This is an argument error")
rescue ex : ArgumentError
puts "An argument error occurred: #{ex.message}"
end
```

Multiple `rescue` blocks can be used to handle different types of exceptions.
In the example below, the first `rescue` block will catch an `ArgumentError`, and the second `rescue` block will catch any other exception.

```crystal
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
rescue
puts "An error occurred!"
end
```

The `begin` block can also have an `else` block, which is executed if no exception is raised.

```crystal
begin
puts "No error occurred"
rescue
puts "An error occurred!"
else
puts "No error occurred"
end
```

Lastly, there is an `ensure` block that is always executed, regardless of whether an exception was raised.

```crystal
begin
raise "This is an error"
rescue
puts "An error occurred!"
ensure
puts "This is always executed"
end
```

## Method convention

Some methods have two versions, one with `!` and the other without.
This can mean two different things.
One is that the method mutates the object, and the other is that the method can raise an exception.

But there is also another convention around ending a method with `?` mentioned in the boolean concept.
Some methods raise an exception by default but also have a version ending with `?` which returns `nil` instead of raising an exception.

This is ideal when you want to avoid an error being raised.
This can benefit performance since it doesn't have to create a stack trace and, if set up correctly, could make the code safer.

## Custom exceptions

You can also create your own exceptions by inheriting from the [`Exception`][exception] class.
In doing so, you can optionally override the `initialize` method to set the exception message.
This can be done by assigning an instance variable named `@message` with the message.

```crystal
class MyException < Exception
def initialize
@message = "This is my exception"
end
end

raise MyException.new
```

[exception-handling]: https://crystal-lang.org/reference/syntax_and_semantics/exception_handling.html
[exception]: https://crystal-lang.org/api/Exception.html
10 changes: 10 additions & 0 deletions concepts/errors/links.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"url": "https://crystal-lang.org/reference/syntax_and_semantics/exception_handling.html",
"description": "Crystal docs: exception-handling"
},
{
"url": "https://crystal-lang.org/api/Exception.html",
"description": "Crystal API: exception"
}
]
7 changes: 7 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,13 @@
"array-methods"
],
"status": "beta"
},
{
"slug": "the-farm",
"name": "the-farm",
"uuid": "600b6035-ca9b-41df-843d-2dcd86cac2d1",
"concepts": [],
"prerequisites": []
}
],
"practice": [
Expand Down
35 changes: 35 additions & 0 deletions exercises/concept/the-farm/.docs/hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Hints

## 1. Divide the food evenly

- Start by writing the function signature of `DivideFood`.
It should accept 2 parameters of type `FodderCalculator` and `int` and return two values of types `float64` and `error`.
Revisit the [functions concept][concept-functions] if you need more information on how to define functions.
- In the function body, call the `FodderAmount` [method][concept-methods] on `FodderCalculator` to fetch the default total amount of fodder for the cows.
It will return the actual result and an error.
Handle the error via an if-statement as it was explained in the introduction.
- After that, call the `FatteningFactor` method and handle the error return value as before.
- Now that you have the fodder amount and the factor, you can calculate the final result.
You need to divide the fodder by the number of cows (revisit [numbers] for hints on type conversion) and multiply with the factor. Check the introduction for what to return as the error value in case of success.

## 2. Check the number of cows

- `ValidateInputAndDivideFood` has the same function signature as `DivideFood`.
- Since you want to return early in case of an error in Go, you first check whether the number of cows is less or equal than 0 with an if-statement.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Since you want to return early in case of an error in Go, you first check whether the number of cows is less or equal than 0 with an if-statement.
- Since you want to return early in case of an error, you first check whether the number of cows is less or equal than 0 with an if-statement.

- If it is, you return an error that you created with `errors.New`.
Make sure the message matches the instructions.
- If the number of cows is valid, you can proceed to call the existing `DivideFood` function from task 1.

## 3. Improve the error handling

- Start by creating the `InvalidCowsError` [struct][concept-structs] with two unexported fields that hold the number of cows and the message.
- Next, define the `Error` method on that struct (with a pointer receiver). Revisit the exercise introduction for help on how to do this.
- Now you can work on the `ValidateNumberOfCows` function.
Depending on the number of cows ([if-statement][concept-conditionals]), it should create and return a new instance of the `InvalidCowsError` and set the correct message while doing so.
If the number of cows was valid, `nil` should be returned.

[concept-methods]: /tracks/go/concepts/methods
[concept-functions]: /tracks/go/concepts/functions
[concept-numbers]: /tracks/go/concepts/numbers
[concept-structs]: /tracks/go/concepts/structs
[concept-conditionals]: /tracks/go/concepts/conditionals-if
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[concept-conditionals]: /tracks/go/concepts/conditionals-if
[concept-conditionals]: /tracks/go/concepts/conditionals-if

Loading