However, I will not call die. I find it frustrating when modules die.

-- Bod in Re^6: STDERR in Test Results

While I doubt Bod, hailing from (working-class) Coventry UK, would be permitted to enter the hallowed Long Room at Lords to hurl abuse at the Australian cricket team during the Ashes test match last weekend, I'm sure he won't be stumped by this meditation's title ... unlike monks from non-cricket-playing nations, doubtless unfamiliar with Bazball :).

Bodball, you may recall I once scolded you for asking "what should I test?" long after you'd released your module. I similarly urge you to get into the habit of thinking long and hard about your module's error handling well before you release it, and for the same reasons. Like TDD, it's crucial to perform this error-handling analysis early because doing so will likely change your module's interface.

Further to the excellent general advice you've already recieved from afoken, I'm interested to learn more about the errors you commonly encounter in practice when using your Business::Stripe::Webhook module. I also urge you to add an expanded "Error Handling" section to your module's documentation.

General Error Handling Advice

Don't fail silently. Failure is inevitable; failing to report failures is inexcusable. Failing silently causes the following problems:

Embrace your software's fallibility. Assume that humans will make mistakes using your software. Try to minimize ways for people to misuse your software, but assume that you can't completely eliminate misuse. Therefore, plan error messages as you design software.

-- General error handling advice from Google Error Messages course

Programming Tips

What should a function do if it cannot perform its allocated task?

Return failure when:

Throw an exception when:

This is not a black and white issue. Experience and good taste are required.

Business::Stripe::Webhook Error Handling

Though unfamiliar with your Business::Stripe::Webhook domain, I briefly browsed your module's documentation. Good to see you've already written a short "Errors and Warnings" section in its documentation; I suggest you improve and expand this section for the next release.

AFAICT, your basic error handling strategy is for your methods to set the error property, for example:

$vars{'error'} = 'Missing payload data'
with the module user expected to check this error property after calling each method. Is that right?

I think a clear statement of your overall error-handling strategy, combined with a couple of real-world examples of handling common errors you've experienced when using your module, would be invaluable to your users ... and may cause you to tweak your module's error-handling code and interface ... which is why this step is ideally performed well before release. :)

See Also

Updated: minor changes to wording were made shortly after posting. Added more references to the See Also section.

Replies are listed 'Best First'.
Re: EyeBall stumps BodBall
by stevieb (Canon) on Jul 07, 2023 at 01:29 UTC

    There is an overwhelming amount of good advice in this post.

    "Don't fail silently."

    That can't be stressed enough, especially when dealing with critical data (in this case, financial).

    "Like TDD, it's crucial to perform this error-handling analysis early because doing so will likely change your module's interface."

    Truer advice has never been given. To me, writing my test platform is the most critical facet to interface design. It dictates it. I'm literally writing tests (nothing more than scripts that are true users of my software) that guides me to the end interface appearance. For me, my error messages (after so many years of coding) are generally logical, but through writing tests to cover edge cases, error messages become more refined, appropriate and understandable.

    "with the module user expected to check this error property after calling each method."

    Few will do that. You'll receive emails and bug tickets. Then you'll have to hand-hold to get someone to repro the issue which will never be the same again, and the bug goes unfound.

    IMHO, a warning is something that you can let your users check for themselves. True errors however should be forefront. A user of the interface should have to do work to work around the error, not hear from someone that something isn't working causing all manner of teams to troubleshoot, then search out the error. Show them up front... somethings broken severely. It might be you, it might be me, but we can't continue until action is taken.

    It is counter-productive to force users to go seek out whether there's an error under the hood or not. Break the caller, and allow them to decide whether they want to put an eval{} around it or not.

    Failing silently is a nightmare and a frustration across the board.

    Magnificent post, eyepopslikeamosquito!

    -stevieb

Re: EyeBall stumps BodBall
by hippo (Archbishop) on Jul 07, 2023 at 07:09 UTC
    Throw an exception when ... an error is so rare that the programmer is likely to forget to check for it

    Yes. Furthermore, I would expand this to include "or you believe that the underlying triggering condition should never occur". I'm sure we have all written code with labyrinthine logical paths and found one branch which should never be reached. In such a scenario I throw an exception in that branch to say as much and briefly indicate why. Such exceptions end up being thrown on a bafflingly frequent basis and this otherwise-unhandled unexpected condition is a really good candidate for an abrupt termination. What else should you do when the logic fails so utterly?

    Failing silently causes the following problems: Users wonder whether something has gone wrong. ("Why did my order not go through?")

    Much worse than "why didn't it go through?" is "has it gone through or not?" - I've encountered this a few times recently when ordering online and it is frankly infuriating. You end up wrestling not only with the vendor's poor interface but also your card provider's only slightly better one trying to work out if you have been charged zero, one or many times and how many widgets, if any, you can expect to turn up in 5 to 7 working days. Such vendors are migrated swiftly to the bargepole list.


    🦛

      > I would expand this to include "or you believe that the underlying triggering condition should never occur" ... What else should you do when the logic fails so utterly?

      Yes. You just reminded me of my first boss, great salesman, engaging personality ... still my favourite boss. Though not a great programmer, he'd hacked out a system in record time and managed to sell it to a chain of bakeries. His system made us a lot of money and was working remarkably well until, in the middle of the night, we get a support call from a very sheepish bakery worker, reluctant to explain exactly what the problem was. Finally, she said the system had stopped with "What the f*!!? am I doing here?" displayed on the screen. :)

Re: EyeBall stumps BodBall (Error Handling)
by eyepopslikeamosquito (Archbishop) on Jul 09, 2023 at 14:01 UTC

      Have exceptions report from the caller's location, not from the place where they were thrown

      I think this is the only one that I'd strongly disagree with.

      If you get an error thrown deep in some module as a result of a call, it is really easy to use Carp::confess to find out which line of your own code triggered it. However when you know only the line of your own code that triggered it, it can be far harder to diagnose what has actually gone wrong - if you can't immediately determine it based on the documentation of the call you made, you really want to find the line of code that raised the exception and work back from there.

        I'm sure hv knows this, but for others: if you want to make a die in another module to actually confess, use Devel::Confess, e.g.
        perl -d:Confess myfailingscript.pl
Re: EyeBall stumps BodBall
by Bod (Parson) on Jul 08, 2023 at 17:46 UTC
    I similarly urge you to get into the habit of thinking long and hard about your module's error handling well before you release it, and for the same reasons.

    You'll be pleased to hear that I thought at length about error handling before I wrote Business::Stripe::WebCheckout. A consistent approach has been carried over into my other Stripe modules:
    Business::Stripe::Subscription
    Business::Stripe::Webhook

    AFAICT, your basic error handling strategy is for your methods to set the error property

    Almost...

    The modules set error in any error. The modules provide the success method which returns true on success or false if there was an error. The user of the module is expected to check success. The error method returns the error property so that the user can understand why success is false.

    These modules are designed to be used on a webserver. As such, calling die is not helpful to anyone. The end user, the person browsing the website, will probably get an HTTP 500 error and the user of the module is unlikely to detect that quickly.

    That's what I meant by: "However, I will not call die. I find it frustrating when modules die"

    Instead, I opted for the default error handing approach of DBI. I use this module in default mode (i.e. with RaiseError false) and only turn on RaiseError for debugging, not for live code.

    I'm not sure my Stripe modules warrant the equivalent of RaiseError in them. I may be wrong here.

    Specifically with Business::Stripe::Webhook, this is designed for building scripts to listen for calls from Stripe to a webhook endpoint. Stripe specifies that the endpoint MUST give a JSON encoded response. The module provides a method for doing this. If the payload from Stripe is invalid or has been changed by the user of the module, then Stripe still needs this reply. If we were to call die then Stripe will not get the reply and may invalidate the endpoint. So not calling die is a very deliberate design decision.

    I think a clear statement of your overall error-handling strategy, combined with a couple of real-world examples of handling common errors you've experienced when using your module, would be invaluable to your users

    Thank you for this, I shall include it in the next release of each of the Stripe modules.

    I work on my own and build modules that are for my use. They only get released if I think they will also be is use for other people. But, like everyone, I suffer from what I call "the curse of knowledge". It is obvious to me what it is supposed to do and how to use it. So it is very difficult to know which bit to explain in full detail and which only need a little explanation.

      The modules set error in any error. The modules provide the success method which returns true on success or false if there was an error. The user of the module is expected to check success. The error method returns the error value so that the user can understand why success is false.

      I see most of your Business::Stripe::Webhook module's methods begin by clearing the module error property via:

      $self->{'error'} = '';
      presumably to avoid confusion from a spurious error property set by an earlier method call. Is that right?

      I also noticed your get_subscription method does not do this. Is this a bug?

      In case it helps, another minor issue I noticed is the return value of your check_signature method is not documented. Examining the code indicates it can return 1 or 0 or undef. It would be good to document this method's return value and the difference between returning 0 and undef.

        presumably to avoid confusion from a spurious error property set by an earlier method call. Is that right?

        Spot on...

        I also noticed your get_subscription method does not do this. Is this a bug?

        Well spotted, yes it is a bug. It has been corrected in my code locally. Although I suppose I should have done that on GitHub!

        In case it helps, another minor issue I noticed is the return value of your check_signature method is not documented

        It helps a lot thank you.
        As it says in the documentation, there shouldn't be any need to call this method. Originally I was going to have it as a private method but decided that there may be a use case for checking Stripe signatures outside of the workflow that's documented. So, in the end it became a public method.

        The return values have now been added to POD in my local copy.

      G'day Bod,

      Here's a few things that I do which you may find useful (possibly in code outside the Business::Stripe:: namespace). In general, these are intended to promote consistency and reduce coding effort.

      Have a small number of routines to output messages (e.g. print_error(), print_warning() and so on). These accept a format for the message along with a variable number of arguments. Here's a very rough, off-the-top-of-my-head example:

      sub print_error { my ($fmt, @args) = @_; die 'ERROR: ', sprintf $fmt, @args; }

      Define each format once only. This could be globally as a constant, specific to a subroutine as a state variable, or using some other method; you may even use a combination of these. Here's some arbitrary examples of formats:

      'Start date (%s) cannot be after end date (%s)' 'Postcode (%s) contains invalid characters' 'ID is a required field'

      You can paste these formats directly into the DIAGNOSTICS (or equivalent) section of your POD. See perldiag for a multitude of examples of this type of documentation.

      Where possible, I try to collect as many problem reports as possible into a single message. So, if the above three example formats were needed, the user would know to make changes to date, postcode and ID fields from a single piece of feedback, rather than being drip-fed one problem at a time.

      How messages are presented will very much depend on the interface. With GUIs (web or other) I often find a small [i] button can provide information about what exactly is required in a field. A popup panel can list the problems and also contain the same [i] buttons for ease of reference.

      — Ken

        Very helpful, thanks Ken

        I have been trying (not always successfully) to have a small number (preferably one) to output everything...this includes a single method as a wrapper around the process method of Template.

        It's reassuring to read your advice which is generally aligned with what I'm striving to do.