Error Handling
PureConfig features a rich error model used on reading operations. Most PureConfig methods that read Scala types from
configurations return a ConfigReader.Result[A]
- an alias for Either[ConfigReaderFailures, A]
, with A
being the type of a
successful result and ConfigReaderFailures
being a non-empty list of errors that caused the reading operation to fail.
From the various types of ConfigReaderFailure
, one of them is of particular interest: a ConvertFailure
is an error
occurred during the conversion process itself. It features a reason (FailureReason
), an optional location in the
config files where the conversion error occurred and a path in the config.
There are several possible FailureReason
s, the most common being:
- A general, uncategorized reason (
CannotConvert
); - A required key was not found (
KeyNotFound
); - A config value has a wrong type (
WrongType
).
For example, given a config like this:
import pureconfig._
import pureconfig.generic.auto._
case class Name(firstName: String, lastName: String)
case class Person(name: Name, age: Int)
case class PersonConf(person: Person)
Trying to load it with a string instead of an object at name
results in a ConvertFailure
because of a WrongType
:
val res = ConfigSource.string("{ person: { name: John Doe, age: 35 } }").load[PersonConf]
// res: ConfigReader.Result[PersonConf] = Left(
// ConfigReaderFailures(
// ConvertFailure(
// WrongType(STRING, Set(OBJECT)),
// Some(ConfigOrigin(String)),
// "person.name"
// ),
// WrappedArray()
// )
// )
All error-related classes are present in the pureconfig.error
package.
Validations in custom readers
When implementing custom readers, the cursor API already deals with the most common reasons for a reader to fail.
However, it also provides a failed
method for users to do validations on their side, too:
import com.typesafe.config.ConfigValueType._
import scala.util.{Try, Success, Failure}
import pureconfig.error._
case class PositiveInt(value: Int) {
require(value >= 0)
}
implicit val positiveIntReader = ConfigReader.fromCursor[PositiveInt] { cur =>
cur.asString.flatMap { str =>
Try(str.toInt) match {
case Success(n) if n >= 0 => Right(PositiveInt(n))
case Success(n) => cur.failed(CannotConvert(str, "PositiveInt", s"$n is not positive"))
case Failure(_) => cur.failed(WrongType(STRING, Set(NUMBER)))
}
}
}
case class Conf(n: PositiveInt)
ConfigSource.string("{ n: 23 }").load[Conf]
// res0: ConfigReader.Result[Conf] = Right(Conf(PositiveInt(23)))
ConfigSource.string("{ n: -23 }").load[Conf]
// res1: ConfigReader.Result[Conf] = Left(
// ConfigReaderFailures(
// ConvertFailure(
// CannotConvert("-23", "PositiveInt", "-23 is not positive"),
// Some(ConfigOrigin(String)),
// "n"
// ),
// WrappedArray()
// )
// )
ConfigSource.string("{ n: abc }").load[Conf]
// res2: ConfigReader.Result[Conf] = Left(
// ConfigReaderFailures(
// ConvertFailure(
// WrongType(STRING, Set(NUMBER)),
// Some(ConfigOrigin(String)),
// "n"
// ),
// WrappedArray()
// )
// )
Custom failure reasons
Users are not restricted to the failure reasons provided by PureConfig. If we wanted to use a domain-specific failure
reason for our PositiveInt
, for example, we could create it like this:
case class NonPositiveInt(value: Int) extends FailureReason {
def description = s"$value is not positive"
}
implicit val positiveIntReader = ConfigReader.fromCursor[PositiveInt] { cur =>
cur.asString.flatMap { str =>
Try(str.toInt) match {
case Success(n) if n >= 0 => Right(PositiveInt(n))
case Success(n) => cur.failed(NonPositiveInt(n))
case Failure(_) => cur.failed(WrongType(STRING, Set(NUMBER)))
}
}
}
ConfigSource.string("{ n: -23 }").load[Conf]
// res3: ConfigReader.Result[Conf] = Left(
// ConfigReaderFailures(
// ConvertFailure(NonPositiveInt(-23), Some(ConfigOrigin(String)), "n"),
// WrappedArray()
// )
// )
Throwing an exception instead of returning Either
In some usage patterns, there isn’t a need to deal with errors as values. For example, a good practice to handle configs
in an application is to load the whole config with PureConfig at initialization time, causing the application to fail
fast in case of a malformed config. For those cases, the loadOrThrow
method can be used instead of load
:
ConfigSource.string("{ n: 23 }").loadOrThrow[Conf]
// res4: Conf = Conf(PositiveInt(23))
ConfigSource.string("{ n: -23 }").loadOrThrow[Conf]
// pureconfig.error.ConfigReaderException: Cannot convert configuration to a repl.MdocSession$MdocApp$Conf. Failures are:
// at 'n':
// - (String: 1) -23 is not positive
//
// at pureconfig.ConfigSource.loadOrThrow(ConfigSource.scala:81)
// at pureconfig.ConfigSource.loadOrThrow$(ConfigSource.scala:78)
// at pureconfig.ConfigObjectSource.loadOrThrow(ConfigSource.scala:92)
// at repl.MdocSession$MdocApp$$anonfun$11.apply(error-handling.md:108)
// at repl.MdocSession$MdocApp$$anonfun$11.apply(error-handling.md:108)
The message of the thrown exception contains human-readable information of all the errors found by PureConfig, with the errors grouped and organized by path.