0

I have determined that in a Python TkInter GUI program, it is best practice to enclose the entire thing in a try / except block in order to catch all exceptions and present them to the end user (as opposed to something going wrong silently or the program exiting for seemingly no reason).

However, this approach has some problems. Consider the following tiny program that attempts to divide by 0 when a button is clicked:

import tkinter

class Foo():
    def __init__(self):
        # Initialize a new GUI window
        root = tkinter.Tk()

        # The "Generic error" message is shown when the following is uncommented
        #number = 1 / 0

        # Define a button and draw it
        button = tkinter.Button(root, text='Generate an error', command=self.generate_error)
        button.pack()

        # Loop forever
        root.mainloop()

    def generate_error(self):
        # The "Generic error" message is not shown
        number = 1 / 0



if __name__ == '__main__':
    try:
        # Run the Foo class
        Foo()

    except Exception as e:
        print('Generic error:', str(e))

Why does the "Generic error" statement not apply to the button callback function?

James
  • 1,394
  • 2
  • 21
  • 31
  • 1
    Because the code that generates the error is not in the `try` block. That is, calling `Foo()` does not raise an exception. – kindall May 27 '16 at 22:48
  • Your comment is unclear. Can you modify the code snippet to show the proper way to enclose everything in a generic try / except block? – James May 27 '16 at 22:52
  • Almost certainly because the UI layer uses threads. Exceptions in other threads are not propagated to the main thread. – Martijn Pieters May 27 '16 at 22:54
  • Why do you think tkinter programs fail silently? If you divide by zero, you'll get a stacktrace. If you want to catch such a problem, you should catch it in the code that is doing the division. You can't just have a "catch all" exception handler, because by the time that handler catches something your GUI will likely be in an unrecoverable state. – Bryan Oakley May 27 '16 at 23:02
  • In the context of a frozen (.exe) GUI program, I don't care that the GUI is in a unrecoverable state; I want the end-user to be able to report the problem to me without having to manually install Python and manually download the source code and manually running the program in a terminal to tell me what the error was. – James May 27 '16 at 23:05
  • @James: your approach won't achieve that. Because by the time your exception handler is actually reached, your UI is hosed. – Martijn Pieters May 27 '16 at 23:07

1 Answers1

5

The following StackOverflow post was helpful: Should I make silent exceptions louder in tkinter?

Basically, I need to use report_callback_exception. I've modified the code snippet accordingly:

import tkinter
import tkinter.messagebox
import traceback

class Foo():
    def __init__(self):
        # Initialize a new GUI window
        tkinter.Tk.report_callback_exception = callback_error  # TkInter callbacks run in different threads, so if we want to handle generic exceptions caused in a TkInter callback, we must define a specific custom exception handler for that
        root = tkinter.Tk()

        # The error() function is triggered when the following is uncommented
        #number = 1 / 0

        # Define a button and draw it
        button = tkinter.Button(root, text='Generate an error', command=self.generate_error)
        button.pack()

        # Loop forever
        root.mainloop()

    def generate_error(self):
        # The "callback_error()" function is triggered when the button is clicked
        number = 1 / 0


def error(message, exception):
    # Build the error message
    if exception is not None:
        message += '\n\n'
        message += traceback.format_exc()

    # Also log the error to a file
    # TODO

    # Show the error to the user
    tkinter.messagebox.showerror('Error', message)

    # Exit the program immediately
    exit()


def callback_error(self, *args):
    # Build the error message
    message = 'Generic error:\n\n'
    message += traceback.format_exc()

    # Also log the error to a file
    # TODO

    # Show the error to the user
    tkinter.messagebox.showerror('Error', message)

    # Exit the program immediately
    exit()



if __name__ == '__main__':
    try:
        # Run the Foo class
        Foo()

    except Exception as e:
        error('Generic error:', e)
Community
  • 1
  • 1
James
  • 1,394
  • 2
  • 21
  • 31