Skip to content

Our API suggests that panicking should be the default #14275

Open
@janhohenheim

Description

@janhohenheim

In spirit of #12660
Adapted from #14268

What problem does this solve or what need does it fill?

I believe it is not controversial to say that when an API offers two similar functions with similar names, the shorter will seem like the default. As such, I believe many people will instinctively gravitate to single and single_mut over get_single and get_single_mut. This means we are subtly pushing users to prefer the implicitly panicking version over the fallible one. This is bad, as it leads to games that may run well on the developers machine but then panic in an edge case.
It is currently understood in the wider Rust community that panics should be an exceptional case, a nuclear option for when recovery is impossible.
We should do our best to make it as easy to do the right thing by default (See Falling Into The Pit of Success). This means that it should be easier to handle an error by propagating it with ? to a logger or similar than to panic. Our current API does the opposite.

This is an issue for the following APIs:

What solution would you like?

Deprecate and remove panicking variants. Improve ergonomics by adding macros for early returns. Using a macro for get_single as an example:

fn do_stuff(primary_window: Query<&Window, With<PrimaryWindow>>) {
    let window = get_single!(primary_window);
    // Do stuff with the window. If there's not exactly one, we have already returned.
}

Which expands to:

fn do_stuff(primary_window: Query<&Window, With<PrimaryWindow>>) {
    match primary_window.get_single() {
        Ok(item) => item,
        Err => return default(),
    };
    // Do stuff with the window. If there's not exactly one, we have already returned.
}

Note that returning default() will allow the macro to work with systems that return an Option and get piped into error handling methods.

Similar macros are already being used by some users, as indicated by https://github.com/tbillington/bevy_best_practices?tab=readme-ov-file#getter-macros

Paraphrasing @alice-i-cecile:

I'm also coming around on the macro approach: I'd rather this be a Rust feature where we can just ? in function that return (), but failing that this is better than the endless let else return pattern.

As the engine matures, I'm increasingly against panicking APIs, especially in seemingly innocuous cases like this. While it's nice for prototyping, it's a serious hazard for refactors and production-grade apps.

We should decide on what to do for all of these areas at the same time and make a consistent decision across the board, pulling in both SME-ECS and @cart. I think they're better implemented as separate PRs to avoid extreme size, but they should be shipped in the same cycle if we make the change.

What alternative(s) have you considered?

  • Do nothing
  • Leave the API as-is and heavily recommend the get_ variants in documentation
  • Print a warning when calling panicking variants
  • Keep the panicking variants and rename them to something clunky like _unchecked
  • Make the panicking API opt-in via a feature
  • Leave the API as-is and let the schedule handle panics
  • Use a #[system] macro that modifies the code to allow us to use ? in it, like in bevy_mod_sysfail

Open Questions

Naming

There is a sentiment that get_ is used by the standard library in general when returning an Option.
Quoting @benfrankel:

This isn't true for the standard library: Vec::first, Iterator::next, Path::parent, Error::source, etc.

The get_ prefix is only used to differentiate from a panicking version. If there's no panicking version, there's no need for the get_ prefix. For some reason Bevy has latched onto foo vs get_foo as the only API choice when foo may fail, but it doesn't have to be this way.

In a world where Bevy is not panicking by default and always hands out Options and Results, I believe there is little reason to stick to a get_ prefix. It is unnecessary noise on otherwise very concise functions. As such, I would advise to change Bevy's naming convention and drop it as part of this initiative. This may sound like something for another issue, but I'll argue that it should be discussed at least in the same release cycle. Users will already have to go and touch every instance of mut, single, etc. so I don't want to further annoy them by having to change them yet a second time when we drop the get_ prefix.

I realize that this is more controversial however and I'm fine with leaving it. I just want to point out that this is probably the best chance we will get to change it.

Macro parameters

It would be nice to have some influence over what should be done in the unhappy path. For example, we could have a parameter that controls an error! log:

get_single!(foo, "Oh heck");

which expands to:

match foo.get_single() {
    Ok(item) => item,
    Err => {
        error!("Oh heck");
        return default();
    }
};

Of course, the use of error! is arbitrary here and the user may want to use warn! instead or something else altogether. To accommodate for this, we could pass a closure:

get_single!(foo, || error!("Oh heck"));

Which expands to the same as above. Note that this closure could even dictate what we return. Since error! returns (), this works out for the common case, and a user is free to build their own Error variant. However at that point, I don't know if a macro call is saving much typing work anymore.

One could also combine these variants by using labeled parameters:

get_single!(foo, warn: "Oh heck");
get_single!(bar, error: "Oh heck");
get_single!(baz, returns: || error!("Oh heck"));

This would allow the user to have a terse version for the common case usage and a more verbose one for custom error handling.

continue

This discussion has focused on the case where we want an early return. I think that is the common case, at least in my experience. What about continue however? Do we want a _continue variant for all macros? An extra parameter? Is there some kind of Rust hack that turns into either continue or return depending on context? Would that even be what the user expects to happen?

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsC-FeatureA new feature, making something new possibleD-ComplexQuite challenging from either a design or technical perspective. Ask for help!M-Needs-Migration-GuideA breaking change to Bevy's public API that needs to be noted in a migration guideS-Ready-For-ImplementationThis issue is ready for an implementation PR. Go for it!X-BlessedHas a large architectural impact or tradeoffs, but the design has been endorsed by decision makers

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions