summaryrefslogtreecommitdiffstats
path: root/content/blog/exception-handling
diff options
context:
space:
mode:
Diffstat (limited to 'content/blog/exception-handling')
-rw-r--r--content/blog/exception-handling/index.md132
1 files changed, 66 insertions, 66 deletions
diff --git a/content/blog/exception-handling/index.md b/content/blog/exception-handling/index.md
index f6aeda4..ce4d58a 100644
--- a/content/blog/exception-handling/index.md
+++ b/content/blog/exception-handling/index.md
@@ -181,32 +181,32 @@ def do_some_analysis!
end
```
-You might have noticed the error in the previous example;
-`return` is misspelled. Although modern editors provide some protection against this specific type of syntax error,
-this example illustrates how `rescue Exception` does harm to our code.
+You might have noticed the error in the previous example;
+`return` is misspelled. Although modern editors provide some protection against this specific type of syntax error,
+this example illustrates how `rescue Exception` does harm to our code.
At no point is the actual type of the exception (in this case a `NoMethodError`) addressed, or intended to be addressed.
## 3. Never `rescue` more exceptions than you need to
-The point 2 is a specific case of this general rule:
-We should always be careful not to over-generalize our exception handlers.
-The reasons are the same; whenever we catch more exceptions than we should,
-we end up hiding parts of the application logic from higher levels of the application,
-not to mention suppressing the developer’s ability to handle the exception themselves.
+The point 2 is a specific case of this general rule:
+We should always be careful not to over-generalize our exception handlers.
+The reasons are the same; whenever we catch more exceptions than we should,
+we end up hiding parts of the application logic from higher levels of the application,
+not to mention suppressing the developer’s ability to handle the exception themselves.
**This severely affects the extensibility and maintainability of the code.**
-If we do attempt to handle different exception subtypes in the same handler,
-we introduce fat code blocks that have too many responsibilities.
-For example, if we are building a library that consumes a remote API, handling a `MethodNotAllowedError` (HTTP 405),
+If we do attempt to handle different exception subtypes in the same handler,
+we introduce fat code blocks that have too many responsibilities.
+For example, if we are building a library that consumes a remote API, handling a `MethodNotAllowedError` (HTTP 405),
is usually different from handling an `UnauthorizedError` (HTTP 401), even though they are both `ResponseError`s.
-As we will see, often there exists a different part of the application
+As we will see, often there exists a different part of the application
that would be better suited to handle specific exceptions in a more [DRY][7] way.
-So, define the [single responsibility][8] of your class or method,
-and **handle the bare minimum of exceptions that satisfy this responsibility requirement**.
-For example, if a method is responsible for getting stock info from a remote a API,
-then it should handle exceptions that arise from getting that info only,
+So, define the [single responsibility][8] of your class or method,
+and **handle the bare minimum of exceptions that satisfy this responsibility requirement**.
+For example, if a method is responsible for getting stock info from a remote a API,
+then it should handle exceptions that arise from getting that info only,
and leave the handling of the other errors to a different method designed specifically for these responsibilities:
```rb
@@ -222,19 +222,19 @@ def get_info
end
end
```
-Here we defined the contract for this method to only get us the info about the stock.
-It **handles endpoint-specific errors**, such as an incomplete or malformed JSON response.
-It doesn’t handle the case when authentication fails or expires, or if the stock doesn’t exist.
-These are someone else’s responsibility, and are explicitly passed up the call stack
+Here we defined the contract for this method to only get us the info about the stock.
+It **handles endpoint-specific errors**, such as an incomplete or malformed JSON response.
+It doesn’t handle the case when authentication fails or expires, or if the stock doesn’t exist.
+These are someone else’s responsibility, and are explicitly passed up the call stack
where there should be a better place to handle these errors in a DRY way.
## 4. Resist the urge to handle exceptions immediately
-This is the complement to the point 3.
-An exception can be handled at any point in the call stack, and any point in the class hierarchy,
-so knowing exactly where to handle it can be mystifying.
-To solve this issue, many developers opt-in to handle any exception as soon as it arises,
-but investing time in thinking this through will usually result
+This is the complement to the point 3.
+An exception can be handled at any point in the call stack, and any point in the class hierarchy,
+so knowing exactly where to handle it can be mystifying.
+To solve this issue, many developers opt-in to handle any exception as soon as it arises,
+but investing time in thinking this through will usually result
in finding a more appropriate place to handle specific exceptions.
@@ -253,12 +253,12 @@ def create
end
end
```
-(Note that although this is not technically an exception handler, functionally, it serves the same purpose,
+(Note that although this is not technically an exception handler, functionally, it serves the same purpose,
[since `@client.save` only returns `false` when it encounters an exception][12].)
-In this case, however, repeating the same error handler in every controller action is the opposite of DRY,
-and damages **maintainability** and **extensibility**.
-Instead, we can make use of the special nature of exception propagation,
+In this case, however, repeating the same error handler in every controller action is the opposite of DRY,
+and damages **maintainability** and **extensibility**.
+Instead, we can make use of the special nature of exception propagation,
and handle them only once, in the [parent controller class][13], `ApplicationController`:
```rb
@@ -281,86 +281,86 @@ def render_unprocessable_entity(e)
end
```
-This way, we can ensure that all of the `ActiveRecord::RecordInvalid` errors
+This way, we can ensure that all of the `ActiveRecord::RecordInvalid` errors
are properly and DRY-ly handled in one place, on the base `ApplicationController` level.
-This gives us the freedom to fiddle with them if we want to handle specific cases at the lower level,
+This gives us the freedom to fiddle with them if we want to handle specific cases at the lower level,
or simply let them propagate gracefully.
## 5. Not all exceptions need handling
-When developing a gem or a library, many developers will try to encapsulate the functionality
-and not allow any exception to propagate out of the library.
+When developing a gem or a library, many developers will try to encapsulate the functionality
+and not allow any exception to propagate out of the library.
But sometimes, it’s not obvious how to handle an exception until the specific application is implemented.
-Let’s take [`ActiveRecord`][14] as an example of the ideal solution.
-The library provides developers with two approaches for completeness.
+Let’s take [`ActiveRecord`][14] as an example of the ideal solution.
+The library provides developers with two approaches for completeness.
The [`save`][15] method handles exceptions without propagating them, simply returning `false`,
-while [`save!`][16] raises an exception when it fails.
-This gives developers the option of handling specific error cases differently,
+while [`save!`][16] raises an exception when it fails.
+This gives developers the option of handling specific error cases differently,
or simply handling any failure in a general way.
-But what if you don’t have the time or resources to provide such a complete implementation?
+But what if you don’t have the time or resources to provide such a complete implementation?
In that case, if there is any uncertainty, it is best to **expose the exception**.
-Here’s why: We are working with changing requirements almost all the time,
-and making the decision that an exception will always be handled in a specific
-way might actually harm our implementation, damaging **extensibility** and **maintainability**,
+Here’s why: We are working with changing requirements almost all the time,
+and making the decision that an exception will always be handled in a specific
+way might actually harm our implementation, damaging **extensibility** and **maintainability**,
and potentially adding huge technical debt, especially when developing libraries.
-Take the earlier example of a stock API consumer fetching stock prices (point 3.).
-We chose to handle the incomplete and malformed response on the spot,
-and we chose to retry the same request again until we got a valid response.
-But later, the requirements might change, such that we must fall back to saved historical stock data,
+Take the earlier example of a stock API consumer fetching stock prices (point 3.).
+We chose to handle the incomplete and malformed response on the spot,
+and we chose to retry the same request again until we got a valid response.
+But later, the requirements might change, such that we must fall back to saved historical stock data,
instead of retrying the request.
-At this point, we will be forced to change the library itself,
-updating how this exception is handled, because the dependent projects won’t handle this exception.
-(How could they? It was never exposed to them before.)
-We will also have to inform the owners of projects that rely on our library.
-This might become a nightmare if there are many such projects,
+At this point, we will be forced to change the library itself,
+updating how this exception is handled, because the dependent projects won’t handle this exception.
+(How could they? It was never exposed to them before.)
+We will also have to inform the owners of projects that rely on our library.
+This might become a nightmare if there are many such projects,
since they are likely to have been built on the assumption that this error will be handled in a specific way.
Now, we can see, **how this situation degrades the library’s usefulness, extensibility, and flexibility.**
So here is the bottom line: **if it is unclear how an exception should be handled, let it propagate gracefully.**
-There are many cases where a clear place exists to handle the exception internally,
-but there are many other cases where exposing the exception is better.
-So before you opt into handling the exception, just give it a second thought.
+There are many cases where a clear place exists to handle the exception internally,
+but there are many other cases where exposing the exception is better.
+So before you opt into handling the exception, just give it a second thought.
**A good rule of thumb is to only insist on handling exceptions when you are interacting directly with the end-user.**
## 6. Follow the convention
-The implementation of Ruby, and, even more so, Rails, follows some naming conventions,
+The implementation of Ruby, and, even more so, Rails, follows some naming conventions,
such as distinguishing between `method_names` and `method_names!` with a “bang.”
-In Ruby, the bang indicates that the method will alter the object that invoked it,
-and in Rails, it means that the method will raise an exception if it fails to execute.
+In Ruby, the bang indicates that the method will alter the object that invoked it,
+and in Rails, it means that the method will raise an exception if it fails to execute.
**Try to respect the same convention, especially if you are going to open-source your library.**
-If we were to write a new `method!` with a bang in a Rails application, we must take these conventions into account.
+If we were to write a new `method!` with a bang in a Rails application, we must take these conventions into account.
There is nothing forcing us to raise an exception when this method fails.
-Another Ruby convention, attributed to [Jim Weirich][17] ([Thanks for everything Jim. We will miss you][18]),
+Another Ruby convention, attributed to [Jim Weirich][17] ([Thanks for everything Jim. We will miss you][18]),
is to use `fail` to indicate method failure, and only to use `raise` if you are re-raising the exception.
> An aside, because I use exceptions to indicate failures,
-> I almost always use the `fail` keyword rather than the `raise` keyword in Ruby.
-> Fail and raise are synonyms so there is no difference except that `fail`
-> more clearly communicates that the method has failed.
-> The only time I use `raise` is when I am catching an exception and re-raising it,
-> because here I’m not failing, but explicitly and purposefully raising an exception.
+> I almost always use the `fail` keyword rather than the `raise` keyword in Ruby.
+> Fail and raise are synonyms so there is no difference except that `fail`
+> more clearly communicates that the method has failed.
+> The only time I use `raise` is when I am catching an exception and re-raising it,
+> because here I’m not failing, but explicitly and purposefully raising an exception.
> This is a stylistic issue I follow, but I doubt many other people do.
[Source][19]
-Many other language communities have adopted conventions like these around how exceptions are treated,
+Many other language communities have adopted conventions like these around how exceptions are treated,
and **ignoring these conventions will damage the readability and maintainability of our code.**
## 7. Logger.log(everything)
-This practice doesn’t solely apply to exceptions,
+This practice doesn’t solely apply to exceptions,
but if there’s one thing that should always be logged, it’s an exception.
-*Special thanks to [Avdi Grimm][20] and his awesome talk [Exceptional Ruby][21],
+*Special thanks to [Avdi Grimm][20] and his awesome talk [Exceptional Ruby][21],
which helped a lot in the making of this article.*
*This article was originally published with the help of an editor on [Toptal in September 2017][22].*