Skip to content

Support Result<T, E> as return type in #[func]#1544

Merged
Bromeon merged 1 commit intomasterfrom
feature/func-result
Apr 15, 2026
Merged

Support Result<T, E> as return type in #[func]#1544
Bromeon merged 1 commit intomasterfrom
feature/func-result

Conversation

@Bromeon
Copy link
Copy Markdown
Member

@Bromeon Bromeon commented Mar 29, 2026

Closes #425.

Result<T, E> can now be returned to Godot. The representation on Godot's side depends on E and can be tweaked with an ErrorToGodot trait.

For now, only a few E types are directly supported:

  • Result<T, Unexpected> is converted as T to Godot. In the error case, the function call will print a Godot error. The calling code will either abort the function (in GDScript with varcall and Godot Debug builds) or receive a default value.
  • Result<T, ()> returns a Variant which is either T::to_variant() or nil.
  • Result<(), global::Error> returns the global::Error enum.

You could customize this with your own E types:

  • Result<T, MyGodotResult> could return a custom class that requires checking for errors before accessing the value.
  • Result<Array<T>, MyGenericArray> could emulate an Option<T> like type in Godot: [] on error, [T] on success. Typed but no error value.
  • ...

Example usage:

#[godot_api]
impl PlayerCharacter {
    // Verifies that required nodes are present in the developer-authored scene tree.
    // Missing nodes are a scene setup bug, not an expected runtime condition.
    #[func]
    fn init_player(&mut self) -> Result<(), strat::Unexpected> {
        // Node missing = scene setup bug.
        let Some(hand) = self.base().try_get_node_as::<Node3D>("Skeleton3D/Hand") else {
            func_bail!("Player {}: 3D model is missing a hand", self.id);
        };

        // HashMap loaded at startup; missing or unparseable values are a code bug.
        let max_health = self.config_map
            .get("max_health")
            .ok_or("'max_health' key missing in config")?  // &str
            .parse::<i64>()?;                              // ParseIntError

        // Initialize self with hand + max_health...
        Ok(())
    }

    #[func]
    fn do_fallible_work(&self) -> Result<(), global::Error> {
        // ...
        Ok(())
    }
}

I'd like to keep this PR limited in scope, we can add more building blocks for common errors later. But it's important that we get the foundation somewhat right.

One thing that's not too nice is that some impls need an extra bound ToGodot<Via: Clone>. I should have made GodotConvert::Via: Clone an implied bound, but now that needs to wait until v0.6. Unless I add it directly on GodotType... 🧐 Edit: solved in #1545

@Bromeon Bromeon added feature Adds functionality to the library c: ffi Low-level components and interaction with GDExtension API labels Mar 29, 2026
@Bromeon Bromeon added this to the 0.5.x milestone Mar 29, 2026
@GodotRust
Copy link
Copy Markdown

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1544

@ogapo
Copy link
Copy Markdown
Contributor

ogapo commented Mar 30, 2026

This is great! Would it be possible to support Result<T, impl std::error::Error> rather than String? That would make it easier to interpret most of the common error types as well as user custom ones without specific error marshalling.

@musjj
Copy link
Copy Markdown

musjj commented Mar 30, 2026

Why not have an anyhow-style error type to which all error types can be coerced to?

Bevy employs a similar approach using a universal error type called BevyError. The implementation itself is pretty short.

If gdext adopts this approach, users can write their fallible functions like this:

#[func]
fn do_fallible_work(&self) -> Result<(), GodotError> {
    let a: Result<(), FooError> = do_a();
    a?;

    let b: Result<(), BarError> = do_b();
    b?;

    let c: Result<(), BazError> = do_c();
    c?;

    Ok(())
}

Similar to anyhow, Bevy also provides a type alias as an ergonomic enhancement:

pub type Result<T = (), E = BevyError> = Result<T, E>;

Applied to gdext, this makes it so that users can write their function signatures like this:

#[func]
fn do_fallible_work_a(&self) -> Result {
    // ...
    Ok(())
}


#[func]
fn do_fallible_work_b(&self) -> Result<u32> {
    // ...
    Ok(5)
}

@Bromeon
Copy link
Copy Markdown
Member Author

Bromeon commented Mar 30, 2026

@ogapo and @musjj very good ideas, thanks! I initially planned to not introduce too broad conversions, but maybe I overestimated their complexity... I'll definitely check how this would look 👍

I guess such error types would still map to the "fatal" path, i.e. GDScript sees T and fails the call if Err(E) is returned?

Maybe we can then also start without String for now 🤔

@musjj
Copy link
Copy Markdown

musjj commented Mar 30, 2026

Yeah mapping it to the fatal path sounds reasonable for now!

In the future, maybe an option to register global/per-function error handlers (relevant?) could be added.

A nice use case for this is creating a global error handler that panics in debug builds, but only logs a warning in release builds.

@Bromeon
Copy link
Copy Markdown
Member Author

Bromeon commented Mar 30, 2026

This came up on Reddit as well 🙂 so yes, something to consider!

@Bromeon Bromeon force-pushed the feature/func-result branch 2 times, most recently from f2bc9c7 to 8a3f1fd Compare March 31, 2026 09:39
@Bromeon
Copy link
Copy Markdown
Member Author

Bromeon commented Mar 31, 2026

I added the FuncError type which does exactly this. The name is more specific than GodotError, as its main purpose is to be returned from #[func]. There is also func_bail!("message") to exit the function fatally.

Updated PR description with a usage example.

Comment thread godot-core/src/meta/error/strat/bail.rs Outdated
@Bromeon Bromeon force-pushed the feature/func-result branch 5 times, most recently from c6826c4 to 9480105 Compare April 11, 2026 16:03
@Bromeon
Copy link
Copy Markdown
Member Author

Bromeon commented Apr 11, 2026

I renamed the "fatal" error strategy to Unexpected, which describes the error handling strategy best -- don't use it for runtime errors, as it cannot be reliably handled in calling code. Returning this implies a bug, which should be fixed during development.

There are now two more possible types E in the Result<T, E> return type:

  • () means on error, Variant::nil() is returned. This is effectively a nullable return value, but sacrifices type safety (declared type becomes Variant).
  • global::Error enum for the constant-based error.

I proposed a few other strategies (Variant, Dictionary, Array01) but decided to not yet add them until there's clearer feedback on what's needed. Please let us know how using Result goes. Don't hesitate to experiment with custom types `E. If some turn out to be recurring, we may consider adding new strategies.

Future work may include configuring hooks occurring during errors (linking this feature to #411).

@Bromeon Bromeon force-pushed the feature/func-result branch 6 times, most recently from ee0ae0c to ec49994 Compare April 13, 2026 21:57
@Bromeon Bromeon force-pushed the feature/func-result branch 2 times, most recently from 3c3fd56 to d760781 Compare April 15, 2026 16:44
Adds `ErrorToGodot` trait to customize error mapping strategies.
Supports a few common ones out of the box.
@Bromeon Bromeon force-pushed the feature/func-result branch from d760781 to 3e39db8 Compare April 15, 2026 17:41
@Bromeon Bromeon added this pull request to the merge queue Apr 15, 2026
Merged via the queue into master with commit f8a28c7 Apr 15, 2026
23 checks passed
@Bromeon Bromeon deleted the feature/func-result branch April 15, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: ffi Low-level components and interaction with GDExtension API feature Adds functionality to the library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

#[func]: map Result return types to Godot

5 participants