1

Problem

I have a Python library which sends XML requests to an external API. If an issue occurs, the API responds with an error containing an error code and description with error details.

These errors can be caused by anything from malformed XML data, an invalid username/password, editing a read-only attribute, or trying to load data that doesn't exist.

<Response Status="Failure" Action="LoadByName">
    <Error Message="WFP-00235 The job name does not exist in the database." ErrorCode="17" AtIndex="0"/>
</Response>

Current approach

Because I can't control the response I'll receive, my library currently handles any exceptions by raising a RuntimeError with the message received from the API.

import xml.etree.ElementTree as ElementTree

def parse_response(xml):
    # Check if XML response contains any errors
    if xml.find(".//Error") is not None:
        raise RuntimeError(xml.find(".//Error").get("Message"))
    ...

RuntimeError: WFP-00235 The job name does not exist in the database.

One major flaw with this approach is that the ErrorCode isn't preserved when an exception is raised. This makes it impossible to catch specific API errors, as each error is essentially a generic exception.

Question

How should I handle XML errors that come from an external API? Can I raise/catch errors based on their numeric ErrorCode?

  • Is it guaranteed that a <Response Status="Failure"> always contains an <Error> element? If not, I'd also raise an unspecific exception for that Failure Response case, as otherwise they might go unnoticed. – Ralf Kleberhoff Feb 1 '18 at 18:53
  • @RalfKleberhoff Good catch. Not all Failure responses include an <Error> element, so a more generic exception would be good to have. – Stevoisiak Feb 1 '18 at 18:59
4

I would suggest that you avoid using a general exception in this case. General "something happened" type exceptions are fine for situations where there is nothing reasonable you can do about it.

In this case you seem to care about the various errors. The simplest way to do this is to create a single exception type that can hold the all the error data.

class UhOhError(Exception):
  def __init__(self, message, errorcode, errordata):
    super(UhOhError, self).__init__(message)
    self.errorcode = errorcode
    self.errordata = errordata

Then you have access to those fields wherever you want to catch it. If you know what all of these errors are going to be and have different actions you want to take given the various problems, you might want to have various exception types. That makes the exception handling easier because you declare which errors you want to catch in various places simply the except clause alone.

You can even do both, a single exception type for most things and a specific type for issues that you need to single out and treat differently.

  • My current solution is to catch, check the error number, then reraise if it's not an expected error. Is there a cleaner solution? try: login(username, password) except UhOhError as error: if error.errorcode = '<number>': print("Incorrect password") else: raise – Stevoisiak Feb 7 '18 at 15:04
  • It really depends on what you are trying to do but i would probably determine the exception type at the point where you get the errors from the Reponse. Filter for any expected errors and create a corresponding Exception class or classes. Anything else would be put in a general 'something happened' Exception type. Then you throw whatever you created. Then in the code that calls to this, you catch each type of exception at the place that makes the most sense. Usually unexpected error should bubble-up farther than ones that you can deal with. – JimmyJames Feb 7 '18 at 21:48
1

You are starting this from the wrong side. You shouldn't ask "should I throw a RuntimeError, or what else should I do". You should ask "what should my application do if error X happens".

I generally categorise errors like this: There are errors where your app can fix the problem. For example a status 300 (redirection) where you should know how to fix it. In that case, you fix the problem.

There are errors where the user can fix the problem (and likely wants to fix the problem). In that case you tell the user, and help them as far as possible to solve the problem. Typical situation is no WiFi connection - that's a problem the user can fix.

Then there are errors that might fix themselves if you try again later. Depending on the situation the app tries again some time later, just on its own, or tells the user, and possibly tries again later. For example, a timeout likely means that a server is down, and there should be someone running around like mad fixing the problem for you, and later it will work.

Then there are errors that indicate you made a mistake using the API. That would happen a lot during development, if it happens in production then you can't do much other than telling the user and giving up.

And there are all the errors where you have no idea how to handle them. There's nothing to make it work, so you can only report these errors and fail gently.

  • This is true for handling an exception after it has been raised. However, my library backend needs to raise an exception first before the user application can decide how to handle it. – Stevoisiak Apr 24 '18 at 20:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.