When you start developing your first Scala projects, you may sometimes miss out to address one common problem with pattern matching, and that is error handling. Basically, you may pass a type to a method used for matching and that type doesn't really match any pattern, which will lead to a runtime error.
Say, you created an abstract class Filter
, a few case classes that inherit Filter
and provide a specific filter
type, and a method filterValues()
that accepts the filter parameter and uses pattern matching to verify some input.
Here's what the described implementation might look like:
1 2 3 4 5 6 7 8 9 10 |
|
Now your colleague, also a Scala developer, creates their own case class that extends Filter
to verify some value. But
they may forget to add their class as a case parameter in all places where it's used. Eventually, the Scala
application you're both working on may fail with the MatchError
runtime exception. Needless to say that runtime errors
are really bad.
You should always pay attention to this kind of errors when using pattern matching because you can never know when your application may break.
In this article, we have a look at a few ways to handle the MatchError
runtime exception in Scala:
- Adding a default case
_
to catch all unmatched options - Using the keyword
sealed
with the base class or trait for matching options (although this is a workaround not a problem solver) - Changing the compiler options to stop compilation whenever there are warnings produced thanks to
sealed
Step by step, we review the listed ways to address or mitigate the problem.
Using a default case with pattern matching
The first implementation of filterValues()
missed possible variants of the parameter type, which is why the compiler
won't be able to notice an error. You also won't be able to properly process input parameters.
When using pattern matching, however, you can match an unexpected parameter with an "all-type" option:
1 2 3 4 5 |
|
With the implementation above, you can apply a default filter _
(the last case), an irrefutable pattern, which is
similar to the default case in Java switch statements. _
matches any kind of data you throw into the method.
To handle the mismatch with case _
, you can throw an error, log out a message to the console, or process a case any
other way according to the needs of your application.
This solution isn't perfect, though, as you probably want to keep the method implementation without any extra cases.
Using sealed
to get warnings when a pattern doesn't match
Scala provides a way to generate warnings whenever some pattern doesn't match. That is, you can use the keyword sealed
when defining your abstract class (or trait) for pattern matching. Marking a class with sealed
tells that the subtypes
must be declared in the same file to ensure that all of them are known.
This is how the updated code looks:
1 2 3 4 5 6 7 8 9 10 |
|
If you try to run your code with a sealed
class, the Scala compiler will warn you about the issue:
1 2 3 4 5 6 |
|
However, just placing the keyword sealed
before a class doesn't actually solve the problem. You do get cleaner code
compared to the use of the irrefutable pattern, though. And to find the problematic use of pattern matching, you can
simply inspect the console output to find warnings.
In order not to miss MatchError
, you need to configure the Scala compiler to throw errors whenever this exception
occurs.
So we want the compiler stop the application compilation and say something like: "Look, man, you have a method that
calls your filter but these inputs have failed. Check them, please." The compiler setting you're looking for is called
-Xfatal-warnings
, which fails the compilation if there are any warnings:
1 2 3 |
|
Once you set this options and use sealed
with your abstract classes for pattern matching, the Scala compiler will
always stop building your project whenever a warning is produced, which is great.
Removing annoying warnings
Our talk about the Scala best practices for pattern matching would be incomplete without a solution to ignore warnings
when we don't need them. Often, you can understand from the context that some unexpected type will never be passed as a
parameter to your method, and you're sure that MatchError
won't be thrown. But if you're using sealed
, warnings will
still be shown in the console.
Have a look a this example:
1 2 3 4 5 |
|
Some method apply()
that can match a parameter move
to the defined cases for, say, Car
objects, may not use
all move patterns by design, as shown in the example below, thus producing annoying warnings. To remove them, you can
use the annotation @unchecked
in the selector:
1 2 3 4 |
|
This last example ensures that deep pattern matching won't be executed. Remember to use it with care, as it may lead to downsides described in the beginning of the post.
That’s all you need to know about handling the pattern matching MatchError
runtime exception in your Scala
application.