Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: add componentDidCatch for error handling #60

Merged
merged 7 commits into from
Feb 16, 2024

Conversation

bchelkowski
Copy link
Member

@bchelkowski bchelkowski commented Feb 12, 2024

BREAKING CHANGE: Add enableKopytkoComponentDidCatch bs_const to the manifest file

What did you implement:

The new Lifecycle method componentDidCatch is invoked when the error is thrown within the component.

How did you implement it:

componentDidCatch(error as Object, info as Object)

  sub componentDidCatch(error as Object, info as Object)
    ' The Roku exception object
    ' https://developer.roku.com/docs/references/brightscript/language/error-handling.md#the-exception-object
    ?error
    ' The info object containing
    ' componentMethod - component method where the error has been thrown
    ' componentName - node name that extends KopytkoGroup or KopytkoLayoutGroup
    ?info
  end sub

How can we verify it:

Create a component, add componentDidCatch method, and make the component throw an error.

componentDidCatch implementation

sub componentDidCatch(error as Object, info as Object)
  ?info
  ?error
end sub

example of throwing an errorcomponentDidMount implementation

sub componentDidMount()
  variable = m.some.not.existing.nested.object
end sub

Todos:

  • Write documentation (if required)
  • Fix linting errors
  • Enable "Allow edits from maintainers" for this PR
  • Update the messages below

Is this ready for review?: YES
Is it a breaking change?: YES

@bchelkowski bchelkowski requested a review from a team as a code owner February 12, 2024 16:37
@bchelkowski bchelkowski requested review from tbazelczuk and shirishapitta and removed request for a team February 12, 2024 16:37
@tomek-r
Copy link
Contributor

tomek-r commented Feb 12, 2024

@bchelkowski I heard that try/catch has a huge performance penalty and should be used for instance for third-party libs. I did not benchmark myself. Have you?

What is the motivation to have such handler? When something breaks and app crashes its better to let the box crash rather to supress. At least in dev env.

@bchelkowski
Copy link
Member Author

bchelkowski commented Feb 12, 2024

@tomek-r

I heard that try/catch has a huge performance penalty and should be used for instance for third-party libs. I did not benchmark myself. Have you?

I will try to benchmark it somehow. Could you link some resources that say so? I want to review them.

What is the motivation to have such handler? When something breaks and app crashes its better to let the box crash rather to supress. At least in dev env.

The motivation is to be able to address better app crashes.
This solution is not mandatory for Kopytko Components. Without writing the componentDidCatch method, the app will just behave as it was before (it will crash).
But if someone prefers to handle such crashes in their way, for example by showing a screen with an error message, or reporting the reason for the crash to their destination reporting platform.

So it does not answer the question should whether the app should crash or not, just gives the possibility to choose what should happen.

@bchelkowski
Copy link
Member Author

bchelkowski commented Feb 13, 2024

I heard that try/catch has a huge performance penalty and should be used for instance for third-party libs. I did not benchmark myself. Have you?

I measured the most "fat" components in our application (using roTimespan). Of course, I don't have a big test group so it is not statistically the best approach, but if the performance of try catch would be bad or tragic I would see some differences, but I don't see any, with this solution, and without it looks the same.

@tomek-r
Copy link
Contributor

tomek-r commented Feb 13, 2024

@bchelkowski

Could you link some resources that say so? I want to review them.

I think it was some discussion with Roku support. Have you reviewed https://developer.roku.com/en-gb/docs/references/brightscript/language/error-handling.md#proper-usage-of-exceptions?

I measured the most "fat" components in our application (using roTimespan). Of course, I don't have a big test group so it is not statistically the best approach, but if the performance of try catch would be bad or tragic I would see some differences, but I don't see any, with this solution, and without it looks the same.

It looks like it is optimized for no crash scenario.

  1. I am wondering if catching can be optional. In your proposal, if I don't override the componentDidCatch function try/catch is still executed. Can this be optional?
  2. Does rethrowing change the source stack, for example shows the the file and line where you rethrow and not where the original crash happened?

@bchelkowski
Copy link
Member Author

bchelkowski commented Feb 13, 2024

@tomek-r

I think it was some discussion with Roku support. Have you reviewed https://developer.roku.com/en-gb/docs/references/brightscript/language/error-handling.md#proper-usage-of-exceptions?

Yes, I based on this documentation.

I am wondering if catching can be optional. In your proposal, if I don't override the componentDidCatch function try/catch is still executed. Can this be optional?

Yes, we can introduce bs_const in the app manifest (however the bs_const is mandatory so it will be a breaking change).

Does rethrowing change the source stack, for example shows the the file and line where you rethrow and not where the original crash happened?

Good point, a crash is pointing to the rethrown line, which won't be a transparent solution.
So if we will introduce this feature it will be required to do it with bs_const.

@tomek-r
Copy link
Contributor

tomek-r commented Feb 13, 2024

@bchelkowski Can this be turned off by default?

I am wondering if we can have such handler per component meaning I can for instance have for a chosen component but not for all if bs_const feature flag is on.

@bchelkowski
Copy link
Member Author

@tomek-r

What do you think about this one?
3987423

Without bs_const this implementation makes componentDidCatch transparent for current implementations.

Copy link
Contributor

@tomek-r tomek-r left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bchelkowski Wondering if we can inject #const to a file. But this would require each file modification via plugin?

Comment on lines 151 to 166
sub _functionCall(func as Function, args = [] as Object, context = Invalid as Object)
if (context = Invalid) then context = GetGlobalAA()

argumentsNumber = args.count()
context["$$functionCall"] = func

if (argumentsNumber = 0)
context["$$functionCall"]()
else if (argumentsNumber = 1)
context["$$functionCall"](args[0])
else if (argumentsNumber = 2)
context["$$functionCall"](args[0], args[1])
end if

context.delete("$$functionCall")
end sub
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kopytko utils have function call function

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, will add utils function

Comment on lines 145 to 147
end try
else
_functionCall(func, args, context)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Less nesting if return is appended and no need for else.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for a return if there is else. 😃

I can change to return. It doesn't matter to me.

@@ -2,6 +2,7 @@ sub init()
m.state = {}
m.elementToFocus = Invalid

m._enabledErrorHandling = Type(componentDidCatch) <> "<uninitialized>"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
m._enabledErrorHandling = Type(componentDidCatch) <> "<uninitialized>"
m._enabledErrorCatching = Type(componentDidCatch) <> "<uninitialized>"

What do you think?

Without bs_const I won't be able to catch original error (during development for example). Working with supressed or rethrown errors might be a nightmare

Copy link
Member Author

@bchelkowski bchelkowski Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think?

I prefer Handling, but it doesn't matter to me so much, I can change to Catching.

Without bs_const I won't be able to catch original error (during development for example). Working with supressed or rethrown errors might be a nightmare

But if someone is suppressing the error for some reason, it shouldn't crash.

Some app components could always extend KopytkoGroup and have any logic the dev wants, it could be embedded there, so for example it could throw an error if they want.
And use this component instead of the KopytkoGroup.

For example, JavaScript suppressed error won't be thrown.

Copy link
Member Author

@bchelkowski bchelkowski Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomek-r

For example, with the current solution, if you want to handle an error but have it thrown for the dev environment, you can create your component that will extend KopytkoGroup and use the MyAppGroup component instead of KopytkoGroup.

<?xml version="1.0" encoding="utf-8" ?>

<component name="MyAppGroup" extends="KopytkoGroup">
  <script type="text/brightscript" uri="MyAppGroup.brs" />
</component>
sub componentDidCatch(error as Object, info as Object)
  #if isDevelopmentEnvironment
    ?info
    throw error
  #end if

  ' ... normal production code
end sub

Of course, it won't be the ideal solution as the backtrace of the crash won't be full,
but you still have the original error message and the original line that crashed.
The app is stopped also in the component scope (8085 port), so maybe you are not exactly at the line of the function that crashed, but you can check the component context and variables.

It is a trade-off solution I won't argue with that, but it is also an optional one, I don't want to force anybody to use it.

Maybe I will have some idea in the future how it could be improved but, don't have it now.

@bchelkowski
Copy link
Member Author

@bchelkowski Wondering if we can inject #const to a file. But this would require each file modification via plugin?

I don't really follow. You mean some kind of new comment that will be converted to some kind of code value?

@tomek-r
Copy link
Contributor

tomek-r commented Feb 15, 2024

@bchelkowski

I don't really follow. You mean some kind of new comment that will be converted to some kind of code value?

So in Kopytko.brs file we can add a const with the value true/false and then depending on the value catch the function executions.

#const shouldUseTryCatch = false

sub initKopytko(dynamicProps = {} as Object, componentsMapping = {} as Object)
    if (m._isInitialized) then return

    m._kopytkoDOM.componentsMapping = componentsMapping

    m._previousProps = _cloneObject(dynamicProps)
    m.top.observeFieldScoped("focusedChild", "focusDidChange")
    m.top.update(dynamicProps)

    #if shouldUseTryCatch
        try 
            constructor()
         catch error
             componendDidCatch(error)
        end try
    #else
        constructor()
    #end if
    m._previousState = _cloneObject(m.state) ' required because of setting default state in constructor()

    _mountComponent()

    m._isInitialized = true
end sub

Something like that

# const shouldUseTryCatch = false

Would have to be appended during build time.

Also that means new flag would have to be needed for the config like useTryCatch: true/false

@bchelkowski
Copy link
Member Author

Ok, but why actually? If someone doesn't define componentDidCatch for the component it won't use try/catch.

@tomek-r
Copy link
Contributor

tomek-r commented Feb 16, 2024

Image the requirement. On prod env I want to report errors to some external service and degradate grafecully so I define componentDidCatch. On dev env I want to have full, original stack in order to debug. Your proposition does not allow to do that. I don't want to comment out all componentDidCatch functions. I want to switch it off for all on dev env without modifying the source code.

@bchelkowski bchelkowski changed the title feat: add componentDidCatch for error handling feat!: add componentDidCatch for error handling Feb 16, 2024
@bchelkowski
Copy link
Member Author

bchelkowski commented Feb 16, 2024

@tomek-r

Image the requirement. On prod env I want to report errors to some external service and degradate grafecully so I define componentDidCatch. On dev env I want to have full, original stack in order to debug. Your proposition does not allow to do that. I don't want to comment out all componentDidCatch functions. I want to switch it off for all on dev env without modifying the source code.

3b31ef8

Copy link
Contributor

@tomek-r tomek-r left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good stuff

@bchelkowski bchelkowski merged commit ea32b0e into master Feb 16, 2024
1 check passed
@bchelkowski bchelkowski deleted the feat--add-componentDidCatch branch February 16, 2024 11:30
github-actions bot pushed a commit that referenced this pull request Feb 16, 2024
# [3.0.0](v2.1.5...v3.0.0) (2024-02-16)

* feat!: add componentDidCatch for error handling (#60) ([ea32b0e](ea32b0e)), closes [#60](#60)

### BREAKING CHANGES

* Add enableKopytkoComponentDidCatch bs_const to the manifest file
Copy link

🎉 This PR is included in version 3.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants