HPR3392: Structured error reporting




Hacker Public Radio show

Summary: Initial state When I originally wanted a unified error reporting on the server-side, I made one huge type that enumerated all the possible error cases that could be reported: -- | Error codes for all errors returned by API data ErrorCode -- common error codes = ResourceNotFound | InsufficientRights | FailedToParseDataInDatabase -- errors specific to news | SpecialEventHasAlreadyBeenResolved | UnsupportedArticleType | SpecialNewsExtractionFailed | TriedToMakeChoiceForRegularArticle -- errors specific to simulation state | SimulationStatusNotFound | DeltaTIsTooBig | TurnProcessingAndStateChangeDisallowed | SimulationNotOpenForCommands | SimulationNotOpenForBrowsing -- errors specific to people | StatIsTooLow Text | CouldNotConfirmDateOfBirth | DateOfBirthIsInFuture | FirstNameIsEmpty | FamilyNameIsEmpty | CognomenIsEmpty | RegnalNumberIsLessThanZero -- errors specific to new person creation | AgeBracketStartIsGreaterThanEnd | PersonCreationFailed deriving (Show, Read, Eq) Then I had some helper functions to turn any value of that type into a nice error message: errorCodeToStatusCode :: ErrorCode -> Int statusCodeToText :: Int -> ByteString errorCodeToText :: ErrorCode -> Text raiseIfErrors :: [ErrorCode] -> HandlerFor App () errorCodeToStatusCode was responsible for turning ErrorCode into http status code. For example StatIsTooLow "intrigue" would be 400. statusCodeToText would take this code and turn it into short error message given in http response. 400 would be Bad Request. errorCodeToText would give a bit more verbose explanation of what happened, StatIsTooLow "intrigue" would be mapped to "Stat intrigue is too low". Finally raiseIfErrors would take a list of ErrorCode and use these helper functions to turn them into a http response with correct status code, error message and json body detailing all errors that had happened: [ { code: { tag: "StatIsTooLow" , contents: "intrique" } , error: "Stat intrigue is too low" } ] There’s two tags: code, which contains machine readable details about the error and error, which contains error message that can be shown to user. While this worked fine, there was some problems with it. ErrorCode type was growing larger and larger and the module it was defined in was referred all over the codebase. Every time I added a new error message, all the modules that used error reporting had to be compiled and it was getting slow. Solution Breaking up the ErrorCode to smaller types and moving them to different modules would means less modules were going to built when I added a new error code. The problem was that raiseIfErrors :: [ErrorCode] -> HandlerFor App () wanted a list of ErrorCode and elements in a list have to be of same type. I started by splitting ErrorCode to smaller types. Each of the smaller error types have automatically derived toJSON and fromJSON functions for serializing them to and from JSON: data PersonCreationError = StatIsTooLow Text | CouldNotConfirmDateOfBirth | DateOfBirthIsInFuture | FirstNameIsEmpty | FamilyNameIsEmpty | CognomenIsEmpty | RegnalNumberIsLessThanZero deriving (Show, Read, Eq) $(deriveJSON defaultOptions ''PersonCreationError) That $(deriveJSON defaultOptions ''PersonCreationError) is template haskell call. Basica