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

ADD error catching #109

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open

Conversation

ninovanhooff
Copy link
Collaborator

@ninovanhooff ninovanhooff commented Mar 9, 2025

What is it

A proc that will execute a provided lambda and catch any errors. The exception is printed to the console.
Optionally, the error is marked as fatal, causing the simulator to be paused and the error message to be printed in red.

This is manly intended to add default error catching to the main update loop, but it can also be used in game code if desired. Sample usage is added

Why

  • I've been using this in Wheelsprung and it has been very helpful to me. At the same time this was super challenging to get working when you have no experience with Nim.
  • Lowers the barrier of entry for transitioning from Lua to Nim as this provides behavior that is provided by the lua sdk but absent from a default C setup

Challenges

  • I wasn't sure how to wrap both procs that have a return type and procs without return type in a single wrapper, so I made 2
  • I wanted to wrap the handler call in the initSdk macro too but that is executed very early in the boot sequence and I couldn't figure that out

Out of scope

I plan a follow-up PR where the error is printed to the screen (device only, since simulator has the Console view). This provides a similar crash experience to the lua sdk and allows players to share a photo of the stacktrace with the dev, as is common practice for lua games.

@ninovanhooff ninovanhooff requested a review from Nycto March 9, 2025 10:15
discard runCatchingTyped(
fun = testErrorProc,
fatal = false,
messagePrefix = "Intentional Error: "
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

remove ": "

@@ -33,9 +36,41 @@ proc getSecondsSinceEpoch* (this: ptr PlaydateSys): tuple[seconds: uint, millise
type PDCallbackFunction* = proc(): int {.raises: [].}
var updateCallback: PDCallbackFunction = nil

proc runCatchingTyped*[T](fun: () -> T, fatal: bool = false, messagePrefix: string=""): T =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Nycto
Is there some magic we can add to get rid of the separate typed and untyped variant using a template, macro, or auto typing?

Copy link
Collaborator

Choose a reason for hiding this comment

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

You should be able to implement a single version of this by using a template like this:

template runCatching*(fatal: bool = false, messagePrefix: string = "", body: typed): auto =
    try:
        body
    except:
        getCurrentException().logException(fatal, messagePrefix)
        default(typeof(body))

try:
message = &"{messagePrefix}: {getCurrentExceptionMsg()}\n{exception.getStackTrace()}"
# replace line number notation from (90) to :90, which is more common and can be picked up as source link
message = message.replace('(', ':')
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Apply this replacement to the stacktrace only

Copy link
Collaborator Author

@ninovanhooff ninovanhooff Mar 9, 2025

Choose a reason for hiding this comment

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

...Or perhaps remove it completely.

Vscode terminal can pick up line numbers in parentheses and IntelliJ can do it too when the "Awesome Console" plugin is installed
Screenshot 2025-03-09 at 11 57 59

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm fine either way

if fatal:
playdate.system.error("FATAL:" & getCurrentExceptionMsg())

proc runCatchingVoid*(fun: () -> void, fatal: bool = false, messagePrefix: string = "") =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

"fun" might not be a very descriptive name for non-Kotlin devs

Copy link
Collaborator

Choose a reason for hiding this comment

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

body is the usual name used in Nim

@ninovanhooff
Copy link
Collaborator Author

@Nycto This one is a bit rough still, see my comments.

I thought I'd first make a PR to see if you'd agree to this idea before polishing it.

Note there is also a question for you in there regarding a possibility to have a single proc for both typed and untyped usage.

proc runCatchingTyped*[T](fun: () -> T, fatal: bool = false, messagePrefix: string=""): T =
try:
return fun()
except:
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should pull all of this exception logging code into its own proc, with a signature like this:

proc logException*(exception: ref Exception, fatal: bool = false, messagePrefix: string = "")

let exception = getCurrentException()
var message: string = ""
try:
message = &"{messagePrefix}: {getCurrentExceptionMsg()}\n{exception.getStackTrace()}"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use exception.msg instead of getCurrentExceptionMsg

except:
let exception = getCurrentException()
var message: string = ""
try:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need this try/catch?

if fatal:
playdate.system.error("FATAL:" & getCurrentExceptionMsg())

proc runCatchingVoid*(fun: () -> void, fatal: bool = false, messagePrefix: string = "") =
Copy link
Collaborator

Choose a reason for hiding this comment

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

body is the usual name used in Nim

@@ -33,9 +36,41 @@ proc getSecondsSinceEpoch* (this: ptr PlaydateSys): tuple[seconds: uint, millise
type PDCallbackFunction* = proc(): int {.raises: [].}
var updateCallback: PDCallbackFunction = nil

proc runCatchingTyped*[T](fun: () -> T, fatal: bool = false, messagePrefix: string=""): T =
Copy link
Collaborator

Choose a reason for hiding this comment

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

You should be able to implement a single version of this by using a template like this:

template runCatching*(fatal: bool = false, messagePrefix: string = "", body: typed): auto =
    try:
        body
    except:
        getCurrentException().logException(fatal, messagePrefix)
        default(typeof(body))

try:
message = &"{messagePrefix}: {getCurrentExceptionMsg()}\n{exception.getStackTrace()}"
# replace line number notation from (90) to :90, which is more common and can be picked up as source link
message = message.replace('(', ':')
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm fine either way

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

Successfully merging this pull request may close these issues.

2 participants