0
# Error Handling
1
2
Circe provides comprehensive error handling with detailed failure information for both parsing and decoding operations. The error system tracks operation history and provides clear error messages.
3
4
## Error
5
6
Base class for all circe errors.
7
8
```scala { .api }
9
sealed abstract class Error extends Exception {
10
override def fillInStackTrace(): Throwable = this
11
}
12
```
13
14
## ParsingFailure
15
16
Represents JSON parsing failures from malformed JSON strings.
17
18
```scala { .api }
19
final case class ParsingFailure(message: String, underlying: Throwable) extends Error {
20
override def getMessage: String = message
21
override def toString(): String = s"ParsingFailure: $message"
22
}
23
```
24
25
### ParsingFailure Companion Object
26
27
```scala { .api }
28
object ParsingFailure {
29
implicit val eqParsingFailure: Eq[ParsingFailure]
30
implicit val showParsingFailure: Show[ParsingFailure]
31
}
32
```
33
34
## DecodingFailure
35
36
Represents JSON decoding failures with detailed path and error information.
37
38
```scala { .api }
39
sealed abstract class DecodingFailure extends Error {
40
// Error information
41
def message: String
42
def history: List[CursorOp]
43
def reason: DecodingFailure.Reason
44
def pathToRootString: Option[String]
45
46
// Message creation
47
override def getMessage: String
48
override def toString: String
49
50
// Error manipulation
51
def copy(message: String = message, history: => List[CursorOp] = history): DecodingFailure
52
def withMessage(message: String): DecodingFailure
53
def withReason(reason: DecodingFailure.Reason): DecodingFailure
54
}
55
```
56
57
### DecodingFailure Companion Object
58
59
```scala { .api }
60
object DecodingFailure {
61
// Constructors
62
def apply(message: String, ops: => List[CursorOp]): DecodingFailure
63
def apply(reason: Reason, ops: => List[CursorOp]): DecodingFailure
64
def apply(reason: Reason, cursor: ACursor): DecodingFailure
65
def fromThrowable(t: Throwable, ops: => List[CursorOp]): DecodingFailure
66
67
// Pattern matching support
68
def unapply(error: Error): Option[(String, List[CursorOp])]
69
70
// Type class instances
71
implicit val eqDecodingFailure: Eq[DecodingFailure]
72
implicit val showDecodingFailure: Show[DecodingFailure]
73
74
// Failure reasons
75
sealed abstract class Reason
76
object Reason {
77
case object MissingField extends Reason
78
case class WrongTypeExpectation(expectedJsonFieldType: String, jsonValue: Json) extends Reason
79
case class CustomReason(message: String) extends Reason
80
}
81
}
82
```
83
84
## Errors
85
86
Convenience exception for aggregating multiple errors.
87
88
```scala { .api }
89
final case class Errors(errors: NonEmptyList[Error]) extends Exception {
90
def toList: List[Error]
91
override def fillInStackTrace(): Throwable = this
92
}
93
```
94
95
## Error Companion Object
96
97
```scala { .api }
98
object Error {
99
implicit val eqError: Eq[Error]
100
implicit val showError: Show[Error]
101
}
102
```
103
104
## Usage Examples
105
106
### Parsing Errors
107
108
```scala
109
import io.circe._
110
import io.circe.parser._
111
112
// Invalid JSON string
113
val malformedJson = """{"name": "John", "age":}"""
114
115
parse(malformedJson) match {
116
case Left(ParsingFailure(message, underlying)) =>
117
println(s"Parsing failed: $message")
118
println(s"Underlying cause: ${underlying.getMessage}")
119
case Right(json) =>
120
println("Parsing succeeded")
121
}
122
123
// Result: ParsingFailure: expected json value got '}' (line 1, column 24)
124
```
125
126
### Decoding Errors
127
128
```scala
129
import io.circe._
130
import io.circe.parser._
131
132
case class Person(name: String, age: Int, email: String)
133
134
implicit val personDecoder: Decoder[Person] = Decoder.forProduct3("name", "age", "email")(Person.apply)
135
136
val json = parse("""{"name": "John", "age": "thirty"}""").getOrElse(Json.Null)
137
138
json.as[Person] match {
139
case Left(DecodingFailure(message, history)) =>
140
println(s"Decoding failed: $message")
141
println(s"Path: ${CursorOp.opsToPath(history)}")
142
println(s"History: $history")
143
case Right(person) =>
144
println(s"Decoded: $person")
145
}
146
147
// Result:
148
// Decoding failed: Got value '"thirty"' with wrong type, expecting number
149
// Path: .age
150
// History: List(DownField(age))
151
```
152
153
### Missing Field Errors
154
155
```scala
156
import io.circe._
157
import io.circe.parser._
158
159
case class Person(name: String, age: Int, email: String)
160
implicit val personDecoder: Decoder[Person] = Decoder.forProduct3("name", "age", "email")(Person.apply)
161
162
val json = parse("""{"name": "John", "age": 30}""").getOrElse(Json.Null)
163
164
json.as[Person] match {
165
case Left(failure @ DecodingFailure(message, history)) =>
166
println(s"Missing field error: $message")
167
println(s"Reason: ${failure.reason}")
168
failure.reason match {
169
case DecodingFailure.Reason.MissingField =>
170
println("Field is missing from JSON")
171
case DecodingFailure.Reason.WrongTypeExpectation(expected, actual) =>
172
println(s"Expected $expected but got ${actual.name}")
173
case DecodingFailure.Reason.CustomReason(msg) =>
174
println(s"Custom error: $msg")
175
}
176
case Right(person) =>
177
println(s"Decoded: $person")
178
}
179
```
180
181
### Custom Error Messages
182
183
```scala
184
import io.circe._
185
186
implicit val positiveIntDecoder: Decoder[Int] = Decoder[Int]
187
.ensure(_ > 0, "Age must be positive")
188
.withErrorMessage("Invalid age value")
189
190
val negativeAge = Json.fromInt(-5)
191
negativeAge.as[Int] match {
192
case Left(failure) =>
193
println(failure.message) // "Invalid age value"
194
case Right(age) =>
195
println(s"Age: $age")
196
}
197
```
198
199
### Validation Errors
200
201
```scala
202
import io.circe._
203
204
case class Email(value: String)
205
206
implicit val emailDecoder: Decoder[Email] = Decoder[String].emap { str =>
207
if (str.contains("@") && str.contains("."))
208
Right(Email(str))
209
else
210
Left("Invalid email format")
211
}
212
213
val invalidEmail = Json.fromString("notanemail")
214
invalidEmail.as[Email] match {
215
case Left(failure) =>
216
println(s"Validation failed: ${failure.message}")
217
case Right(email) =>
218
println(s"Valid email: ${email.value}")
219
}
220
```
221
222
### Accumulating Errors
223
224
```scala
225
import io.circe._
226
import io.circe.parser._
227
import cats.data.ValidatedNel
228
import cats.syntax.apply._
229
230
case class Person(name: String, age: Int, email: String)
231
232
implicit val nameDecoder: Decoder[String] = Decoder[String].ensure(_.nonEmpty, "Name cannot be empty")
233
implicit val ageDecoder: Decoder[Int] = Decoder[Int].ensure(_ > 0, "Age must be positive")
234
implicit val emailDecoder: Decoder[String] = Decoder[String].emap { str =>
235
if (str.contains("@")) Right(str) else Left("Invalid email")
236
}
237
238
implicit val personDecoder: Decoder[Person] = new Decoder[Person] {
239
def apply(c: HCursor): Decoder.Result[Person] = {
240
(
241
c.downField("name").as[String](nameDecoder),
242
c.downField("age").as[Int](ageDecoder),
243
c.downField("email").as[String](emailDecoder)
244
).mapN(Person.apply)
245
}
246
247
override def decodeAccumulating(c: HCursor): Decoder.AccumulatingResult[Person] = {
248
(
249
c.downField("name").as[String](nameDecoder).toValidatedNel,
250
c.downField("age").as[Int](ageDecoder).toValidatedNel,
251
c.downField("email").as[String](emailDecoder).toValidatedNel
252
).mapN(Person.apply)
253
}
254
}
255
256
val badJson = parse("""{"name": "", "age": -5, "email": "invalid"}""").getOrElse(Json.Null)
257
258
badJson.asAccumulating[Person] match {
259
case cats.data.Valid(person) =>
260
println(s"Decoded: $person")
261
case cats.data.Invalid(errors) =>
262
println("Multiple errors:")
263
errors.toList.foreach(error => println(s" - ${error.message}"))
264
}
265
266
// Result:
267
// Multiple errors:
268
// - Name cannot be empty
269
// - Age must be positive
270
// - Invalid email
271
```
272
273
### Error Recovery
274
275
```scala
276
import io.circe._
277
278
// Try multiple decoders
279
implicit val flexibleStringDecoder: Decoder[String] =
280
Decoder[String] or Decoder[Int].map(_.toString) or Decoder[Boolean].map(_.toString)
281
282
// Recover from specific errors
283
implicit val recoveryDecoder: Decoder[Int] = Decoder[Int].handleErrorWith { failure =>
284
failure.reason match {
285
case DecodingFailure.Reason.WrongTypeExpectation(_, json) if json.isString =>
286
Decoder[String].emap(str =>
287
try Right(str.toInt)
288
catch { case _: NumberFormatException => Left("Not a number") }
289
)
290
case _ => Decoder.failed(failure)
291
}
292
}
293
294
val stringNumber = Json.fromString("42")
295
stringNumber.as[Int](recoveryDecoder) match {
296
case Right(num) => println(s"Recovered: $num") // "Recovered: 42"
297
case Left(error) => println(s"Failed: ${error.message}")
298
}
299
```
300
301
### Path Information
302
303
```scala
304
import io.circe._
305
import io.circe.parser._
306
307
val nestedJson = parse("""
308
{
309
"users": [
310
{"profile": {"name": "John", "age": "invalid"}}
311
]
312
}
313
""").getOrElse(Json.Null)
314
315
val cursor = nestedJson.hcursor
316
val result = cursor
317
.downField("users")
318
.downN(0)
319
.downField("profile")
320
.downField("age")
321
.as[Int]
322
323
result match {
324
case Left(failure) =>
325
println(s"Error at path: ${failure.pathToRootString.getOrElse("unknown")}")
326
println(s"Operation history: ${failure.history}")
327
println(s"Cursor path: ${CursorOp.opsToPath(failure.history)}")
328
case Right(age) =>
329
println(s"Age: $age")
330
}
331
332
// Result:
333
// Error at path: .users[0].profile.age
334
// Operation history: List(DownField(users), DownN(0), DownField(profile), DownField(age))
335
// Cursor path: .users[0].profile.age
336
```
337
338
### Custom Error Types
339
340
```scala
341
import io.circe._
342
343
sealed trait ValidationError
344
case class TooYoung(age: Int) extends ValidationError
345
case class InvalidEmail(email: String) extends ValidationError
346
347
implicit val ageDecoder: Decoder[Int] = Decoder[Int].emap { age =>
348
if (age >= 18) Right(age)
349
else Left(s"Too young: $age")
350
}
351
352
implicit val emailDecoder: Decoder[String] = Decoder[String].ensure(
353
email => email.contains("@") && email.length > 5,
354
"Invalid email format"
355
)
356
357
case class User(age: Int, email: String)
358
implicit val userDecoder: Decoder[User] = Decoder.forProduct2("age", "email")(User.apply)
359
360
val json = parse("""{"age": 16, "email": "bad"}""").getOrElse(Json.Null)
361
json.as[User] match {
362
case Left(failure) =>
363
println(s"Validation failed: ${failure.message}")
364
println(s"At: ${failure.pathToRootString.getOrElse("root")}")
365
case Right(user) =>
366
println(s"Valid user: $user")
367
}
368
```
369
370
### Working with Either and Option
371
372
```scala
373
import io.circe._
374
375
// Decoder that never fails
376
implicit val safeStringDecoder: Decoder[Option[String]] =
377
Decoder[String].map(Some(_)).handleErrorWith(_ => Decoder.const(None))
378
379
// Either for multiple possible types
380
implicit val stringOrIntDecoder: Decoder[Either[String, Int]] =
381
Decoder[String].map(Left(_)) or Decoder[Int].map(Right(_))
382
383
val json = Json.fromInt(42)
384
385
json.as[Option[String]] match {
386
case Right(None) => println("Not a string, but that's OK")
387
case Right(Some(str)) => println(s"String: $str")
388
case Left(_) => println("This shouldn't happen with safe decoder")
389
}
390
391
json.as[Either[String, Int]] match {
392
case Right(Left(str)) => println(s"String: $str")
393
case Right(Right(num)) => println(s"Number: $num")
394
case Left(error) => println(s"Neither string nor int: ${error.message}")
395
}
396
```