- End users. Example: form validation. Generally feedback about errors in inputs.
- Developers. Interested in debugging. Stack traces, intermediate values, etc.
- Fail fast: stop as soon as you hit an error
- Error accumulation: report all the errors. Typically when validating inputs, typically aimed at end users.
Usually error reporting proceeds in stages. Accumulate all errors in a stage. If not errors proceed to next stage otherwise fail.
Example:
- validate form input
- save stuff to the database
- do some action
Each stage only proceeds to next if there are no errors.
- Exceptions, try, and catch
- Watchdog / New Relic
Try
,Either
Exceptions create stack traces which are great for debugging.
Throwable
is the base class for all exceptions on the JVM.
Error
is for exceptions that you can’t do anything about (e.g. out of memory). Don’t catch Errors
!
Exception
is for everything else.
throw
to raise an exception
try
/ catch
/ finally
to handle exceptions
Match on a tag (which may or may not be equal to the type)
finally
only runs for side effects
You get stack traces (useful for developers) Nothing in the type system forces you to handle errors you want to handle Break substitution
Recommendation:
- Exceptions are ok for developers only errors (i.e. nothing gets reported to the user beyond “something went wrong”)
- Not very good for user errors: type system doesn’t help get this right, very unstructured, cannot accumulate errors
A class in the Scala standard library scala.util.Try
. A value or an exception. An algebraic data type.
- sign is variance, which we skipped over.
Basically a wrapper that can hold an exception or a value.
The constructor for Try
will catch exceptions. Turns exceptions into values, maintains substitution.
A lot of methods on Try
:
map
flatMap
(???)getOrElse
- etc.
We get stack traces from exceptions, we can interoperate with code that produces exceptions. We work with values so we aren’t breaking substitution. We don’t get structured errors and we don’t get accumulation.
Left is by convention the error / failure case (boo!) Right is the success case
Same understanding of map
and flatMap
.
We can store structured data in the failure case—good for presenting errors to user.
We don’t get easy utilities for working with exceptions (e.g. the constructor doesn’t catch exceptions by default)
flatMap
is still fail fast.
Error type must be something can be sensibly “added” together. Technically: must have a Semigroup. Good example is cats.data.NonEmptyList
.
Use parMapN
on a tuple of Either
Import cats.implicits
.
Protip: mapN
/ parMapN
builds on an operation called product
that we have already seen (e.g. on animations / transducers)
Type class coherence.
Type equation for Map:
F[A] map (A => B) = F[B]
type equation. F
is the container type and does not change. The element type can change.
F = Try
map
transforms a value when we have a success. Note it cannot introduce a failure!
F[A] flatMap (A => F[B]) = F[B]
type equation. F
is the container type and does not change.
Notice f
returns a Try
and therefore can introduce a failure.
The expanded understanding of map
and flatMap
is:
- we’re talking about sequencing operations. Operations that work in a defined order. This comes after that.
- we have values in some context (where
F
is the context) map
transforms a value in a context without changing the context.flatMap
can change the context