Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Robust Error Handling in Scala
import scalaz.\/
import scalaz.syntax.either._
object Example2 {
// This example simulates error handling for a simple three tier web application
//
// The tiers are:
// - the HTTP service
// - a user authentication layer
// - a database layer
//
// Each stage defines its own error types
// A few type aliases for clarity
type Query = String
type Host = String
type Username = String
type Password = String
final case class User(username: Username, firstName: String, lastName: String, password: Password)
object HttpService {
sealed trait HttpError extends Exception
final case class CouldNotAuthenticate(username: Username) extends HttpError
// We don't store any infomration about the cause of an internal
// error as we don't want to leak internal information to
// attackers.
final case object InternalError extends HttpError
// After calling login we would have a conversion to some kind of
// HTTP response (not shown). The type is:
// \/[HttpError, User] => HttpResponse
// or in abstract
// A => B
// Readers of Essential Scala should recognise this as a fold
def login(username: Username, password: Password): \/[HttpError, User] =
// We use leftMap to convert the error type from the layer below
// into the error type used at this layer. This is boilerplate to
// some extent, but there are good reasons for changing how we
// report errors, which we see here as we want to avoid leaking
// information to attackers. In a real system we'd also have
// some kind of logging to record information we need for
// debugging.
AuthenticationService.login(username, password).leftMap {
// Don't leak information about why a login attempt
// failed. Letting an attacker know they have found a valid
// username allows them to focus attacks.
case AuthenticationService.NotFound(username) =>
CouldNotAuthenticate(username)
case AuthenticationService.BadPassword(username, password) =>
CouldNotAuthenticate(username)
case AuthenticationService.DatabaseError(error) =>
InternalError
}
}
object AuthenticationService {
sealed trait ServiceError
final case class BadPassword(username: Username, password: Password) extends ServiceError
final case class NotFound(username: Username) extends ServiceError
final case class DatabaseError(error: DatabaseService.DatabaseError) extends ServiceError
def login(username: Username, password: Password): \/[ServiceError, User] =
for {
user <- DatabaseService.findUserByUsername(username).leftMap {
case DatabaseService.NotFound(u) => NotFound(u)
case e @ DatabaseService.CouldNotAuthenticate(u, p) => DatabaseError(e)
case e @ DatabaseService.CouldNotConnect(h) => DatabaseError(e)
}
_ <- checkPassword(user, password)
} yield user
def checkPassword(user: User, password: Password): \/[ServiceError, User] =
if(user.password == password)
user.right
else
BadPassword(user.username, password).left
}
object DatabaseService {
sealed trait DatabaseError
final case class NotFound(username: Username) extends DatabaseError
final case class CouldNotConnect(host: Host) extends DatabaseError
final case class CouldNotAuthenticate(username: Username, password: Password) extends DatabaseError
def findUserByUsername(username: Username): \/[DatabaseError, User] =
// Simulate network and system errors
Math.random() match {
case p if p < 0.1 => CouldNotConnect("database.server").left
case p if p < 0.2 => CouldNotAuthenticate("user", "password").left
case _ =>
username match {
case "noelw" => User("noelw", "Noel", "Welsh", "soverysecret").right
case "daveg" => User("daveg", "Dave", "Gurnell", "\\m/").right
case _ => NotFound(username).left
}
}
}
}
@pjurczenko

This comment has been minimized.

Copy link

pjurczenko commented Apr 5, 2016

This is a great example, but I am really curious about one thing: since login method is impure, how would you test it? Even if we accept that impurity, we can't override DatabaseService object, because AuthenticationService is an object itself. I can think of a few solutions (e.g. making AuthService a trait, or passing \/[DatabaseError, User] as a param), but none of them fully satisfies me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.