2

I would like to improve my coding style with a more robust grasp of try, except and raise in designing API, and less verbose code.

I have nested functions, and when one catches an execption, I am passing the exception to the other one and so on.

But like this, I could propagate multiple checks of a same error. I am referring to: [Using try vs if in python for considering cost of try operation.

How would you handle an error only once across nested functions ?

E.g.

  • I have a function f(key) doing some operations on key; result is passed to other functions g(), h()
  • if result comply with expected data structure, g() .. h() will manipulate and return updated result
  • a decorator will return final result or return the first error that was met, that is pointing out in which method it was raised (f(),g() or h()).

I am doing something like this:

def f(key):
   try:
     #do something
     return {'data' : 'data_structure'}
   except:
     return {'error': 'there is an error'}

@application.route('/')
def api_f(key):
    data = f(k)
    try:
       # do something on data
       return jsonify(data)
    except:
       return jsonify({'error':'error in key'})
Community
  • 1
  • 1
user305883
  • 1,635
  • 2
  • 24
  • 48
  • I found a possible duplicate here: [http://stackoverflow.com/questions/12630224/returning-api-error-messages-with-python-and-flask] However I want to keep question open because I ask a general question on how to pass errors between functions, not necessarily only about returning a response to API. – user305883 Jan 18 '16 at 16:44

1 Answers1

5

IMO try/except is the best way to go for this use case. Whenever you want to handle an exceptional case, put in a try/except. If you can’t (or don’t want to) handle the exception in some sane way, let it bubble up to be handled further up the stack. Of course there are various reasons to take different approaches (e.g. you don’t really care about an error and can return something else without disrupting normal operation; you expect “exceptional” cases to happen more often than not; etc.), but here try/except seems to make the most sense:

In your example, it’d be best to leave the try/except out of f() unless you want to…

raise a different error (be careful with this, as this will reset your stack trace):

try:
    ### Do some stuff
except:
    raise CustomError('Bad things')

do some error handling (e.g. logging; cleanup; etc.):

try:
    ### Do some stuff
except:
    logger.exception('Bad things')
    cleanup()

    ### Re-raise the same error
    raise

Otherwise, just let the error bubble up.

Subsequent functions (e.g. g(); h()) would operate the same way. In your case, you’d probably want to have some jsonify helper function that jsonifies when possible but also handles non-json data:

def handle_json(data):
    try:
        return json.dumps(data)
    except TypeError, e:
        logger.exception('Could not decode json from %s: %s', data, e)

        # Could also re-raise the same error
        raise CustomJSONError('Bad things')

Then, you would have handler(s) further up the stack to handle either the original error or the custom error, ending with a global handler that can handle any error. In my Flask application, I created custom error classes that my global handler is able to parse and do something with. Of course, the global handler is configured to handle unexpected errors as well.

For instance, I might have a base class for all http errors…

### Not to be raised directly; raise sub-class instances instead
class BaseHTTPError(Exception):
    def __init__(self, message=None, payload=None):
        Exception.__init__(self)
        if message is not None:
            self.message = message
        else:
            self.message = self.default_message

        self.payload = payload

    def to_dict(self):
        """ 
        Call this in the the error handler to serialize the
        error for the json-encoded http response body.
        """
        payload = dict(self.payload or ()) 
        payload['message'] = self.message
        payload['code'] = self.code
        return payload

…which is extended for various http errors:

class NotFoundError(BaseHTTPError):
    code = 404 
    default_message = 'Resource not found'

class BadRequestError(BaseHTTPError):
    code = 400 
    default_message = 'Bad Request'

class NotFoundError(BaseHTTPError):
    code = 500 
    default_message = 'Internal Server Error'

### Whatever other http errors you want

And my global handler looks like this (I am using flask_restful, so this gets defined as a method on my extended flask_restful.Api class):

class RestAPI(flask_restful.Api):
  def handle_error(self, e):
      code = getattr(e, 'code', 500)
      message = getattr(e, 'message', 'Internal Server Error')
      to_dict = getattr(e, 'to_dict', None)

      if code == 500:
          logger.exception(e)

      if to_dict:
          data = to_dict()
      else:
          data = {'code': code, 'message': message}

      return self.make_response(data, code)

With flask_restful, you may also just define your error classes and pass them as a dictionary to the flask_restful.Api constructor, but I prefer the flexibility of defining my own handler that can add payload data dynamically. flask_restful automatically passes any unhandled errors to handle_error. As such, this is the only place I’ve needed to convert the error to json data because that is what flask_restful needs in order to return an https status and payload to the client. Notice that even if the error type is unknown (e.g. to_dict not defined), I can return a sane http status and payload to the client without having had to convert errors lower down the stack.

Again, there are reasons to convert errors to some useful return value at other places in your app, but for the above, try/except works well.

Clandestine
  • 1,449
  • 1
  • 13
  • 30
  • Thank you @clandestine - in your third example you catch a `TypeError`, and then raise a `CustomJsonError` : you say that it is the same error (re-raised) : why raised as same one? also referring to: http://stackoverflow.com/questions/13957829/how-to-use-raise-keyword-in-python : suppose I have `g( f(x) )` nested functions, I will have a raise in `f(x)` captured by `g( )` - it looks `f()` is here your second example that will raise error up the stack to `g()` - which is the difference from what you further described as "bubbling up" the error? – user305883 Jan 31 '17 at 10:31
  • The `CustomJsonError` is not the same error re-raised; it was just an arbitrary example of catching one error and then raising your own. What I meant to indicate with that comment is that you alternatively could re-raise the same error that was caught. The point of catching and re-raising the same error in this case would be to log the exception if it's useful to you. If it's not useful to you and if you don't need to do anything in case of the error, you don't need to handle the error at all. – Clandestine Jan 31 '17 at 16:00
  • As for nesting functions, if you pass call `g( f(x) )` and `f(x)` raises an exception, `g()` will never even get called. If you absolutely need `g()` to be called, then `f()` would need to swallow any error. – Clandestine Jan 31 '17 at 16:06