Skip to content

← Error Handling with Exceptions, Retries and Warnings step 3 of 20

Easy Primitives

Catch Errors by Category

A customer clicks Pay, and your code asks the payment gateway to charge their card. The charge can fail in several ways. A declined card needs a specific message, a busy gateway means "try again soon", and a bug in your own code should not look like a payment problem.

The gateway's errors are a family of classes, so you can catch them by category:

PaymentError
├── TransientPaymentError     has .retry_after, in seconds
└── PermanentPaymentError
    └── CardDeclinedError     has .decline_code

An except clause catches its own class and every subclass of it. So except PermanentPaymentError also catches a CardDeclinedError, and except PaymentError catches all four.

checkout("4242")     # "paid: r-4242"
checkout("expired")  # "declined: expired_card"

What to write

Fill in checkout(card). Call charge(card) once, and return the message to show the customer:

  • If charge returns a receipt id, return "paid: <receipt>".
  • For a CardDeclinedError, return "declined: <decline_code>". Read the code from the error's decline_code attribute.
  • For a TransientPaymentError, return "busy: try again in <retry_after>s", for example "busy: try again in 5s".
  • For any other PaymentError, return "failed: <message>", where the message is str(error).
  • Let every other exception reach the caller. A ValueError from charge means your code passed it something that is not a card number. That is a bug, so it should crash, not turn into a payment message.

Python checks except clauses from top to bottom and runs the first one that matches. Order your clauses so that each error gets the message listed above.

The fake gateway

charge(card) decides what happens from the card string:

Card What charge does
"expired" raises CardDeclinedError with code "expired_card"
"no-funds" raises CardDeclinedError with code "insufficient_funds"
"busy" raises TransientPaymentError with retry_after=5
"stolen" raises PermanentPaymentError("card reported stolen")
"closed" raises PaymentError("merchant account closed")
only digits, such as "4242" returns the receipt "r-4242"
anything else raises ValueError

What the driver reports

Keep the error classes, charge and solve as they are. solve(cards=...) calls checkout once per card and returns results, one string per card. If checkout lets an exception escape, the string is "crashed: " followed by the class name, for example "crashed: ValueError".