Beefy Boxes and Bandwidth Generously Provided by pair Networks
The stupid question is the question not asked
 
PerlMonks  

Error handling in a module

by Bod (Parson)
on Feb 17, 2023 at 18:52 UTC ( [id://11150485]=perlquestion: print w/replies, xml ) Need Help??

Bod has asked for the wisdom of the Perl Monks concerning the following question:

I'm writing a module to go with Business::Stripe::WebCheckout that will deal with Stripe subscriptions. (Yes - I have written tests before writing the code!)

The way the previous module deals with errors follows the way DBI mostly does it. If there is an error, it keeps it quiet and carries on. My module relies on the code using it to call success to check the operations worked out and calling error to find out what went wrong.

Currently, the new module uses the same approach. But I am wondering if I should use some other way to detect errors. I've looked back a decade to Re: Error handling and there is some good advice but it doesn't cover modules per see. Also, wisdom can change in a decade...

Here's my reasoning for doing it the way I am. Can you pick fault with it so that together we can find a better way or be confident that the current methodology is fine?

  • As the module connects with Stripe, it is only ever going to be used in a CGI environment. Therefore, the end user will be a web visitor. My concern is not the user experience of the end user. It is that the person who creates the web experience has an easy time of making a good user experience for the end user.
  • Throwing an error to STDERR is not very helpful in a CGI environment
  • Throwing an error with die and stopping the web experience completely is not a very good solution because the error that will be displayed is probably meaningless to the end user.
  • Taking the end user through to the Stripe checkout with no information is not helpful to anyone. So it is better that my module doesn't stop the website from working, it allows the site creator to check for errors and get an idea of what the error is. But, if they choose not to check, it won't take the end user off to Stripe unless all the required information is present.

Maintaining compatibility and consistency with Business::Stripe::WebCheckout is not an issue as there needs to be a new version of that soon. I've learnt a lot since publishing that, a couple of people have made suggestions for extra functionality and Stripe have released a newer version of their API. The new version will adopt the error handling of the new, unpublished as yet Business::Stripe::Subscription.

Replies are listed 'Best First'.
Re: Error handling in a module
by afoken (Chancellor) on Feb 18, 2023 at 10:33 UTC

    The way the previous module deals with errors follows the way DBI mostly does it. If there is an error, it keeps it quiet and carries on. My module relies on the code using it to call success to check the operations worked out and calling error to find out what went wrong.

    Currently, the new module uses the same approach. But I am wondering if I should use some other way to detect errors.

    When I use DBI, I usually enable RaiseError, not only because it is the only sane way to handle failed transactions, but because it exits my program when things go wrong unexpectedly. And every time I enable RaiseError, it slightly annoys me, just like it annoys me that I have to enable strict and warnings. So for me, DBI dies, it does not silently hide errors. There are times when I want different behaviour, e.g. for a brute-force check if a table exists and is accessible. For that, perl has eval:

    unless (eval { my @a=$dbh->selectrow_array('SELECT anycolumn FROM some +table'); 1 }) { $dbh->do('CREATE TABLE sometable ...'); }

    Modules that require manual error checks after each function/method just annoy me. Even more when the functions/methods are incompatible with the well-working open FILE... or die schema, e.g. because they return a true value (e.g. the object itself) even when an error happens.

    As the module connects with Stripe, it is only ever going to be used in a CGI environment. Therefore, the end user will be a web visitor. My concern is not the user experience of the end user. It is that the person who creates the web experience has an easy time of making a good user experience for the end user.

    The end user experience is a problem of the CGI program, not a problem of the module that provides communication with an API.

    Throwing an error to STDERR is not very helpful in a CGI environment

    Directly writing to STDERR is not helpful AT ALL, not just in a CGI environment. Use die on error, use warn to warn of problems. That's it. If the user of your module chooses to ignore errors, the program will be aborted, with an error message written to STDERR. If the user wants some error handling, eval will allow to do any crazy thing you can't even think of. Similar for warnings, $SIG{__WARN__} (see perlvar) allows to handle warnings in ways you can't even think of. If you want to be friendly to the user of your module, provide a way to switch warnings on and off. Probably, warnings should be off by default, unless warnings are globally enabled by -w (i.e. $^W).

    Throwing an error with die and stopping the web experience completely is not a very good solution because the error that will be displayed is probably meaningless to the end user.

    Again, user expoerience is not the problem of a module providing access to an API. eval, Try::Tiny and the new try/catch/finally also work in a CGI environment. If the user of your module chooses to ignore all error handling, the end user will see a "500 Internal Server Error" page. Works as designed. If the user of your module chooses to delegate all error handling to CGI::Carp, the end user will see error message from deep within the application. Works as designed. If the user of your module chooses to wrap the entire code in a big eval, and show an "oops, something went wrong" page with a stupid cat photo in case of any error, the end user will see that. Works as designed. If the user of your module chooses to do reasonable error handling by wrapping functions/methods of your module in eval or try/catch and reacting to individual errors, the CGI can retry or fail with an error message that the end user can understand (e.g. "Sorry, the payment server seems to be down, please try again in five minutes").

    Taking the end user through to the Stripe checkout with no information is not helpful to anyone. So it is better that my module doesn't stop the website from working, it allows the site creator to check for errors and get an idea of what the error is.

    Right. Show the user a message like "Sorry, payment did not work." In parallel, send a message with internal details (i.e. called function and the real error message) to the admin. Easily implemented via eval or try/catch and some e-mail module. Heck, you could use a bugtracking system API to directly report the error into a bugtracking system and give the end user a bug ID ("Sorry, payment did not work. Please contact our support and mention the trouble ticket number 31415926535."). But: That's not your problem. It's a problem of the user of your module, the one writing the CGI. And using die is the sane way to report fatal errors.

    Alexander

    --
    Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)
Re: Error handling in a module
by kcott (Archbishop) on Feb 18, 2023 at 01:56 UTC

    G'day Bod,

    Many modules use a similar system to DBI. Text::CSV has the error_diag() method. The core modules in the IO::Compress:: namespace also do this: IO::Compress::Zip has $ZipError; IO::Compress::Bzip2 has $Bzip2Error; and so on. There are many other examples; it's a tried and tested model; there's certainly nothing wrong with using this approach.

    Some of your points seem to indicate a single action to handle exceptions; I'd suggest there should be at least two: how you present problems to the user and how you present problems to the maintainer.

    I don't know enough about your system but writing to STDERR should generally add to error_log (or similar). This is useful for the maintainer for troubleshooting; it wouldn't normally be available to the user.

    Having some form of error page, or popup error panel, is what you should be presenting to the user. You can add a direction to include the message in a report to a sysadmin; but maintainers shouldn't rely on receiving this.

    In addition to these "at least two", other actions such as emailing a report, or texting a notification, to a sysadmin may be appropriate.

    How you present feedback to users will largely depend on the severity of the problem.

    For a fatal error, an error page might have something like: "Database connection failed. Administrators have been notified. Please try again later.".

    For problems with credentials (authentication or authorisation) you'd generally want to repost the page with an added message. A fairly typical example would be "Username/password mismatch. Please try again.". There might be a timeout or retry limit; it may be appropriate to advise the user of these.

    There are situations where many things need to be validated; in a web context, this will often be a form. I tend to check as much as possible then present the user with a list of problems; e.g. "Name field is required", "Invalid date (31st November)", and so on. This allows the user to fix many things at once; presenting these one at a time can be very annoying. These messages might appear separately against individual form fields; they could be presented together in a list in a popup error panel; you might choose both of those.

    I've aimed to keep my response as general as possible. If you have specific questions, please ask separately.

    — Ken

      Hi Ken

      Thanks as always for your very helpful reply

      Some of your points seem to indicate a single action to handle exceptions; I'd suggest there should be at least two: how you present problems to the user and how you present problems to the maintainer

      You are right, I am thinking here of just one form of error handling...

      This is because I am writing a module that will help others deal with the end user - the person browsing the website. The module is only concerned about providing error reporting to the programmer using it in something bigger. It is up to them to provide suitable messaging to the end user. It is up to me to make it easy for them to do that.

      Of course, I am writing this module because it is something I want to use. So, in the first instance, I will be both using the error reports from the module and serving more helpful error reports to the end user.

Re: Error handling in a module
by eyepopslikeamosquito (Archbishop) on Feb 18, 2023 at 02:38 UTC

    So it is better that my module doesn't stop the website from working, it allows the site creator to check for errors and get an idea of what the error is

    Yes Bod, you're on the right track here. While unit tests help the developer create better quality modules, that is a different problem to remotely supporting a software product!

    As mentioned in passing here, effectively supporting a complex product remotely isn't easy. Unit tests are not enough. You'll need to provide some sort of logging and notification mechanism, without degrading user experience. (For a gentle introduction to Log::Log4perl, see Logging with Log4perl (perlmaven)).

    Unfortunately, I am not qualified to comment on how best to support and troubleshoot a Stripe Payment Gateway. Have you researched how others do it?

      Unfortunately, I am not qualified to comment on how best to support and troubleshoot a Stripe Payment Gateway

      The Stripe Gateway isn't really a problem - it can only do three things. 2 are normal behaviour and one is an error which I handle in the module.

      It's the way my module handles errors that I am concerned with and I feel I am doing it a sensible way. I was more wanting to check that there wasn't a "better" way that I had overlooked and I am now satisfied that the way I've approached it is sensible and useful to anyone losing my module.

      The 'errors' are more 'failed validation' of user input than true errors. So, if the user provides the right data and Stripe doesn't return an HTTP error then there is nothing to report other than success...

Re: Error handling in a module
by NERDVANA (Deacon) on Feb 19, 2023 at 12:10 UTC
    As the module connects with Stripe, it is only ever going to be used in a CGI environment.

    Well, not that many perl devs are using CGI anymore. Is the module not appropriate for Catalyst, Dancer, or Mojo environments?

    In general, I'm in the "code that fails should die and devs should try{}catch{}" camp, with the exception that external inputs to a program should be validated in a way that doesn't involve try/catch so that remote people don't have the ability to trigger __DIE__ handlers on demand.

    But also, continuing my recent Public Service Announcement about the insecurity of HTTP::Tiny, you should add verify_TLS to the HTTP::Tiny constructor before connecting to stripe API.

      Well, not that many perl devs are using CGI anymore. Is the module not appropriate for Catalyst, Dancer, or Mojo environments?

      Sorry - I was using "CGI" to mean "running on a webserver" as opposed to a desktop or elsewhere. I wasn't meaning CGI.

Re: Error handling in a module
by Anonymous Monk on Feb 19, 2023 at 22:41 UTC

    My 2 cents is: This is a good example of why I like to use promise based async code. You promise to do something, and then there is a call back for either having done it (which can be passed a result), or another for having failed to do it (which can be passed an error message). And thus the two paths (success or failure) can bubble up as far as necessary by parent promises and so on.

    In this case its somewhat different because CGI code is meant to execute synchronously: do a particular job, and then finish, passing back a result. So what you are doing is fine: Always return and let the user check the result. They key part is really the 'always return' part - you need to make sure your interface is clear to the end user. One of my biggest gripes about Perl modules in general is that most modules simply throw exceptions because they almost seem to except Perl to be being used at a console by a user executing something synchronously. You have to wrap practically every module in an EVAL statement if you want to make useful web code that is able to handle any error conditions gracefully instead of just crapping itself.

      Always return and let the user check the result. They key part is really the 'always return' part

      That's exactly what I've been doing so far. I was really just wanting to check that there wasn't a "better" way that I'd overlooked.

Re: Error handling in a module
by harangzsolt33 (Chaplain) on Feb 17, 2023 at 20:34 UTC
    Throwing an error with die and stopping the web experience completely is not a very good solution because the error that will be displayed is probably meaningless to the end user.

    Not only that, but I have seen websites where some error in the php code caused not only a cryptic error message to show up but a full dump with the section of source code where the error occurred. I was looking at the source code, and I thought this could be a potential security issue. You don't want your buggy code exposed to hackers. Lol

    2023-02-17: Update: Excuse me, did I say something wrong? Why did you give me -2 point for this comment? Anybody wants to explain this?

    2023-02-17: Update removed.

    2023-02-17: Update reinserted.

    2023-02-17: The words "update" were added. lol :D

      Excuse me, did I say something wrong? Why did you give me -2 point for this comment? Anybody wants to explain this?

      The only possible reason that I can see is that you referred to the OP's code as being buggy, though you haven't seen that code.
      I'm not so sure that actually warrants a downvote, unless you were intentionally trying to provoke the OP. (And I don't think that was your intention.)
      To be clear, I'm not one of the people who downvoted the post.

      Cheers,
      Rob
        I'm sorry, I didn't mean to refer to his code specifically. When I say the word "your code" that's just an expression. I don't know how else to say it. Okay. The point is that it's not a good idea to expose parts of the source code by accident. especially not the part where the error occurred! because it might contain a real vulnerability which could be exploited by a hacker. And in this case the hackers can even see what's going on under the hood. So, that would help them write the exploit. If an error caused the script to display all sorts of cryptic messages with a dump of the source code, then that's a pretty serious error, I would assume. And there is a possibility that this error was caused by the programmer (faulty code maybe). Of course, it could be many other things too. But if we were to write code from scratch, I think it's better to log errors to a file in a CGI environment. Like the OP said, it's not a good idea to die with an error message.

        *Btw when I wrote, "we were to write," I didn't literally mean you and I. It was just a figure of speech.

      G'day harangzsolt33,

      syphilis quoted you as saying:

      "Excuse me, did I say something wrong? Why did you give me -2 point for this comment? Anybody wants to explain this?"

      A question about votes must have occurred after posting; i.e. it's an update.

      As that quote no longer appears in your post, you must have deleted it; i.e. it's another update.

      That's at least two changes to your post without any "Update: ...". There may have been more changes; I don't know. You've been a member for seven years and made hundreds of posts; you should have read "How do I change/delete my post?" by now.

      If people are seeing your post changing without notification, that could be a reason why they downvoted it. That's the reason I downvoted it. Of course, the downvoting by others may have been for other reasons.

      By the way, I wasn't responsible for the "-2"; after I voted, I saw "Reputation: -3 (+2 -5)" — do note that's five monks who have downvoted your post.

      Update: And now I see that quote is back in your post but still without any "Update: ...". You're not doing yourself any favours.

      — Ken

        Okay. Got it!

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://11150485]
Approved by Corion
Front-paged by kcott
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others cooling their heels in the Monastery: (4)
As of 2024-04-20 00:27 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found