Skip to content

Add RETURN_IF_ERR macro, propagating an error upwards #107760

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

Ivorforce
Copy link
Member

Godot does not use exceptions. This often results in patterns where errors are propagated upwards:

Error error = expression();
if (error) {
	return error;
}

I propose to support this paradigm with a dedicated macro:

RETURN_IF_ERROR(expression());

The benefit of this is threefold:

  • Error propagation is encouraged (through the 'syntactic sugar').
  • Existing code is (arguably) easier to read than before.
  • The code is standardized, and if desired, could be improved (e.g. by adding more printing).

An arguable downside is that it's 'another godot-thing' to remember about the code base. I personally think the naming makes it clear enough what the macro does, though.

Relations

The idea of this macro may be familiar to Rust programmers. Rust embraces exception-free coding so much it made this concept into a core part of its grammar, with the question mark ? operator.
It has been proposed to add this operator to C++ too, for exception-free projects like Godot, but it's unlikely it will make it into the standard anytime soon.

If the expression results in an error, it is propagated and returned from the current function.
@Ivorforce Ivorforce added this to the 4.x milestone Jun 20, 2025
@Ivorforce Ivorforce requested review from a team as code owners June 20, 2025 12:01
@Ivorforce Ivorforce requested review from a team as code owners June 20, 2025 12:01
@Ivorforce Ivorforce requested review from a team as code owners June 20, 2025 12:01
@Ivorforce Ivorforce requested review from a team as code owners June 20, 2025 12:01
@Ivorforce Ivorforce removed request for a team June 20, 2025 12:02
@Ivorforce Ivorforce removed request for a team June 20, 2025 12:02
/**
* If the return value of the expression is not OK, returns it as an error.
*/
#define RETURN_IF_ERR_MSG(m_error, m_msg) \
Copy link
Member

Choose a reason for hiding this comment

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

There should probably be an EDMSG version for completeness

@AThousandShips
Copy link
Member

My concern with this is that it's unintuitive, RETURN_IF_ERR doesn't print any message, which you'd expect from the rest of the error macros

I'd say RETURN_IF_ERR_MSG should be named differently to clarify this

Copy link
Member

@AThousandShips AThousandShips left a comment

Choose a reason for hiding this comment

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

Now these messages might not be helpful, but they:

  • Provide a code location for failure, which is important for testing
  • Was meant to trigger an error originally, otherwise the code would have been the if (err != OK) format

So should either be replaced by a new macro with a message, or restored

We could go over some of these discussing if they are worth changing away from errors, but we should assume the original macro was used intentionally

I'm also not sure about the readability in some of these cases

@@ -197,8 +195,7 @@ Error image_to_png(const Ref<Image> &p_image, Vector<uint8_t> &p_buffer) {
}

// trim buffer size to content
Error err = p_buffer.resize(buffer_offset + compressed_size);
ERR_FAIL_COND_V(err, err);
RETURN_IF_ERR(p_buffer.resize(buffer_offset + compressed_size));
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect, this removes an error print

@@ -186,8 +185,7 @@ Error image_to_png(const Ref<Image> &p_image, Vector<uint8_t> &p_buffer) {
ERR_FAIL_COND_V(compressed_size <= png_size_estimate, FAILED);

// write failed due to buffer size, resize and retry
Error err = p_buffer.resize(buffer_offset + compressed_size);
ERR_FAIL_COND_V(err, err);
RETURN_IF_ERR(p_buffer.resize(buffer_offset + compressed_size));
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect, this removes an error print

@@ -173,8 +173,7 @@ Error image_to_png(const Ref<Image> &p_image, Vector<uint8_t> &p_buffer) {
size_t compressed_size = png_size_estimate;
int success = 0;
{ // scope writer lifetime
Error err = p_buffer.resize(buffer_offset + png_size_estimate);
ERR_FAIL_COND_V(err, err);
RETURN_IF_ERR(p_buffer.resize(buffer_offset + png_size_estimate));
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect, this removes an error print

@@ -206,8 +206,7 @@ class CowData {
Error insert(Size p_pos, const T &p_val) {
Size new_size = size() + 1;
ERR_FAIL_INDEX_V(p_pos, new_size, ERR_INVALID_PARAMETER);
Error err = resize(new_size);
ERR_FAIL_COND_V(err, err);
RETURN_IF_ERR(resize(new_size));
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect, this removes an error print

@@ -1886,8 +1835,7 @@ Error encode_variant(const Variant &p_variant, uint8_t *r_buffer, int &r_len, bo

for (const Variant &elem : array) {
int len;
Error err = encode_variant(elem, buf, len, p_full_objects, p_depth + 1);
ERR_FAIL_COND_V(err, err);
RETURN_IF_ERR(encode_variant(elem, buf, len, p_full_objects, p_depth + 1));
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect, this removes an error print


Vector<String> embedded_frameworks = export_plugins[i]->get_apple_embedded_platform_embedded_frameworks();
err = _export_additional_assets(p_preset, p_out_dir, embedded_frameworks, true, true, r_exported_assets);
ERR_FAIL_COND_V(err, err);
RETURN_IF_ERR(_export_additional_assets(p_preset, p_out_dir, embedded_frameworks, true, true, r_exported_assets));
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect, this removes an error print


Vector<String> project_static_libs = export_plugins[i]->get_apple_embedded_platform_project_static_libs();
for (int j = 0; j < project_static_libs.size(); j++) {
project_static_libs.write[j] = project_static_libs[j].get_file(); // Only the file name as it's copied to the project
}
err = _export_additional_assets(p_preset, p_out_dir, project_static_libs, true, false, r_exported_assets);
ERR_FAIL_COND_V(err, err);
RETURN_IF_ERR(_export_additional_assets(p_preset, p_out_dir, project_static_libs, true, false, r_exported_assets));
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect, this removes an error print


Vector<String> apple_embedded_platform_bundle_files = export_plugins[i]->get_apple_embedded_platform_bundle_files();
err = _export_additional_assets(p_preset, p_out_dir, apple_embedded_platform_bundle_files, false, false, r_exported_assets);
ERR_FAIL_COND_V(err, err);
RETURN_IF_ERR(_export_additional_assets(p_preset, p_out_dir, apple_embedded_platform_bundle_files, false, false, r_exported_assets));
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect, this removes an error print

}

Vector<String> library_paths;
for (int i = 0; i < p_libraries.size(); ++i) {
library_paths.push_back(p_libraries[i].path);
}
Error err = _export_additional_assets(p_preset, p_out_dir, library_paths, true, true, r_exported_assets);
ERR_FAIL_COND_V(err, err);
RETURN_IF_ERR(_export_additional_assets(p_preset, p_out_dir, library_paths, true, true, r_exported_assets));
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect, this removes an error print

@@ -174,8 +174,7 @@ Error EditorRun::run(const String &p_scene, const String &p_write_movie, const V
}

OS::ProcessID pid = 0;
Error err = OS::get_singleton()->create_instance(instance_args, &pid);
ERR_FAIL_COND_V(err, err);
RETURN_IF_ERR(OS::get_singleton()->create_instance(instance_args, &pid));
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect, this removes an error print

@Ivorforce
Copy link
Member Author

Ivorforce commented Jun 20, 2025

Now these messages might not be helpful, but they:

  • Provide a code location for failure, which is important for testing
  • Was meant to trigger an error originally, otherwise the code would have been the if (err != OK) format

Agreed. I'd actually argue that all RETURN_IF_ERR calls should print an error and the propagation site. This should make it a lot easier to debug user submitted problems, for all the cases that did not print before.
What do you think?

@AThousandShips
Copy link
Member

I'd avoid replacing non-printing cases, again, we should assume the choice was conscious. It would at least require a more detailed review to assess what and why to change, most of these are near other code that does use error printing.

Some of these are cases where the source code prints an error, and there's little reason to have a print on the caller, so I'd say:

  • It should print where it is the most informative
  • It shouldn't print on multiple levels except when relevant
  • It shouldn't print for cases where the error condition is "normal"

@Ivorforce
Copy link
Member Author

Ivorforce commented Jun 20, 2025

I'd avoid replacing non-printing cases, again, we should assume the choice was conscious. It would at least require a more detailed review to assess what and why to change, most of these are near other code that does use error printing.

Some of these are cases where the source code prints an error, and there's little reason to have a print on the caller, so I'd say:

  • It should print where it is the most informative
  • It shouldn't print on multiple levels except when relevant
  • It shouldn't print for cases where the error condition is "normal"

I anticipated there might be resistance to the ERR_FAIL_COND_V(err, err) change, so it's isolated in a separate commit :)
So yea, I'm open to just using the RETURN_IF_RR part of this PR for now.
(Although your concern above with the expectation to print would be resolved if we added printing in all cases).

Before any action though, I'd suggest we wait for more people to weigh in on this question.

if (error) {
return error;
}
RETURN_IF_ERR(parse_variant(value));
Copy link
Member

Choose a reason for hiding this comment

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

Incorrect as well, this mutates a class variable not a local one, should be:

Suggested change
RETURN_IF_ERR(parse_variant(value));
error = parse_variant(value);
RETURN_IF_ERR(error);

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh man, good catch! I definitely should have noticed that.

for (const String &F : remaps) {
String remap = F;
if (remap == "path") {
String remapped_path = config->get_value("remap", remap);
Vector<uint8_t> array = FileAccess::get_file_as_bytes(remapped_path);
err = save_proxy.save_file(p_udata, remapped_path, array, idx, total, enc_in_filters, enc_ex_filters, key, seed);
RETURN_IF_ERR(save_proxy.save_file(p_udata, remapped_path, array, idx, total, enc_in_filters, enc_ex_filters, key, seed));
Copy link
Member

Choose a reason for hiding this comment

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

This changes the behavior of the loop, which should be investigated, but this changes the behavior, so should be excluded

@AThousandShips
Copy link
Member

AThousandShips commented Jun 20, 2025

Also, as a tangent but relevant due to the introduction of a new macro, I have some concerns about further encouraging moving functional code into checking macros. This isn't a problem in these cases, and it's not a hill I'm dying on, but it's a thing I've been thinking about and growing concerned about in some cases that I think is worth mentioning.

Since we have some macros that are stripped out on certain builds, like DEV_ASSERT it's critical to ensure intentionality with macro use, and while I don't foresee any case where our current ERR_FAIL base macros becoming conditional, we do have those dev only cases, and might want to expand their use, or add further granularity to that

And for that I feel it would be good to reduce the amount of functional code that is put inside these macros in some cases, because:

Err err = foo();
DEV_ASSERT(err == OK);

Is not the same as:

DEV_ASSERT(foo() == OK);

And it might, in the future, be a good idea to fully enforce that these kind of macros should never have functional code in them except when that's explicitly meant to be error only, or even straight up forbid mutating code there at all to prevent some strange edge cases

Just a thought, but relevant to the changes away from having separate calls

Edit:
I also feel that this new macro fails a measure I like to use for macro usage: does the improvement outweigh the cost of losing readability by obscuring code

To me this removes a few lines of code and reduces the number of local declarations, but adds a layer of obscurity

However one thing I think would be valuable would be a set of ERR_FAIL_NOT_OK macros, which do the same but with a good descriptive default message, with the added bonus of normalizing the checks (we currently mix err and err != OK, which makes for confusing error messages), but those could be confusing because they return explicitly, so could be named like ERR_FAIL_NOT_OK_RET for the version that returns the error itself

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

Successfully merging this pull request may close these issues.

2 participants