Universal return pattern

I am not here to hate on AI, nor praise its merits. I am here simply trying to move forward from my experience with Claude. For reasons of my own, I am taking my project back and rebuilding it from the ground up the way I should have done it to begin with. I am using the code Claude and I wrote as a sort of guide, but I am questioning many things. The biggest right now is SSDMResult.
SSDMResult is a universal return object. The idea is that you can return it from every function and know what to expect from every function you ever need to call. But lately, it has been feeling more and more like an anti-pattern.
I wanted to be able to return a text string as well as data from functions. Some functions might return a string as their value or as an error message. I needed a way to tell the difference. SSDMResult holds a message, a severity level, a data variable of type variant, as well as an array called details. Details allows the returning of multiple severity/message pairs. So if you wanted to return a success with a warning, you’d create a SSDMResult.success and add the warning to it.
What do you think of this pattern?

class_name SSDMSeverity
extends RefCounted

enum Level 
{
	SUCCESS,
	INFO,
	WARNING,
	ERROR,
}

class_name SSDMResult
extends RefCounted


var message: String
var data: Variant
var error: Error
var severity: SSDMSeverity.Level
var details: Array[Dictionary] = []


static func success(msg: String = "", data: Variant = null) -> SSDMResult:
	return SSDMResult.new(msg, data, OK, SSDMSeverity.Level.SUCCESS)
	
	
static func print_success(msg: String = "", data: Variant = null) -> SSDMResult:
	printerr("SSDM Success: " + msg)
	print_stack()
	return SSDMResult.new(msg, data, OK, SSDMSeverity.Level.SUCCESS)


static func failure(msg: String = "", data: Variant = null, p_error: Error = FAILED) -> SSDMResult:
	return SSDMResult.new(msg, data, p_error, SSDMSeverity.Level.ERROR)
	
	
static func print_failure(msg: String = "", data: Variant = null, p_error: Error = FAILED) -> SSDMResult:
	printerr("SSDM Error: " + msg)
	print_stack()
	return SSDMResult.new(msg, data, p_error, SSDMSeverity.Level.ERROR)


static func warning(msg: String = "", data: Variant = null, p_error: Error = OK) -> SSDMResult:
	return SSDMResult.new(msg, data, p_error, SSDMSeverity.Level.WARNING)
	
	
static func print_warning(msg: String = "", data: Variant = null, p_error: Error = OK) -> SSDMResult:
	printerr("SSDM Warning: " + msg)
	print_stack()
	return SSDMResult.new(msg, data, p_error, SSDMSeverity.Level.WARNING)


static func info(msg: String = "", data: Variant = null, p_error: Error = OK) -> SSDMResult:
	return SSDMResult.new(msg, data, p_error, SSDMSeverity.Level.INFO)
	
	
static func print_info(msg: String = "", data: Variant = null, p_error: Error = OK) -> SSDMResult:
	printerr("SSDM Info: " + msg)
	print_stack()
	return SSDMResult.new(msg, data, p_error, SSDMSeverity.Level.INFO)


func with_detail(msg: String, sev: SSDMSeverity.Level) -> SSDMResult:
	details.append({"message": msg, "severity": sev})
	return self

	
func with_warning(msg: String) -> SSDMResult:
	return with_detail(msg, SSDMSeverity.Level.WARNING)


func with_info(msg: String) -> SSDMResult:
	return with_detail(msg, SSDMSeverity.Level.INFO)


func with_error(msg: String, err: Error = FAILED) -> SSDMResult:
	error = err
	return with_detail(msg, SSDMSeverity.Level.ERROR)
	
	
func to_failure(msg: String = "", err: Error = FAILED) -> SSDMResult:
	if !msg.is_empty():
		message = msg
	error = err
	severity = SSDMSeverity.Level.ERROR
	return self


func to_success(msg: String = "") -> SSDMResult:
	if !msg.is_empty():
		message = msg
	error = OK
	severity = SSDMSeverity.Level.SUCCESS
	return self


func to_warning(msg: String = "", err: Error = FAILED) -> SSDMResult:
	if !msg.is_empty():
		message = msg
	error = err
	severity = SSDMSeverity.Level.WARNING
	return self


func to_info(msg: String = "", err: Error = FAILED) -> SSDMResult:
	if !msg.is_empty():
		message = msg
	error = err
	severity = SSDMSeverity.Level.INFO
	return self


func merge_from(other: SSDMResult, takeover_message: bool = true) -> SSDMResult:
	if takeover_message:
		message = other.message
		severity = other.severity
		error = other.error
		data = other.data
	elif not other.message.is_empty():
		details.append({"message": other.message, "severity": other.severity})
	for detail in other.details:
		details.append(detail)
	return self


func has_details() -> bool:
	return details.size() > 0


func is_success() -> bool:
	return error == OK


func _init(p_message: String, p_data: Variant, p_error: Error, p_severity: SSDMSeverity.Level) -> void:
	message = p_message
	data = p_data
	error = p_error
	severity = p_severity

I think it’s an over-engineered mess, based on too much programming experience, and not enough experience with Godot.

Error Codes

Godot has a bunch of Error Codes you can already return. If you need to return errors, I recommend using those.

It is a lot easier to do this:

func my_func() -> Error:
	return Error.ERR_BUG

Especially since 99% of functions in Godot do not return anything. This is because in Godot, any variable that is not a primitive data type is passed by Reference. So whenever you pass an argument, you do not need to copy it or return it, just edit it.

Reporting Errors

Godot has the Assert keyword, and if that’s not enough for you, there is GDUnit and Gut for Unit Testing - which is what you should prefer over throwing errors in a game.

Printing Messages

There are already functions for printing errors, warnings and info to the Output panel. Which also works in the condole if you run the debug version of Godot. If you export to debug, you also get all this in a console log - even in web exports with F12.

Your code is a poor version of that existing functionality.

Logs

Godot introduced logging in 4.6 IIRC. Even before that there were a number of logging plugins like LogDuck that were quite excellent if printing messages wasn’t enough.

Conclusion

Your approach is similar to a Python function returning a tuple, or C++/Java/C# where you can use try/catch. These approaches are not really Godot approaches. Typically you change a value, and it either changes, or the game crashes. This is because most things in a game happen 60 times a second or more, so there’s no need for feedback. You just make the change, and in 0.0166 seconds, the next change is coming. It’s like the difference between UDP and TCP/IP. (Where Godot operates more like UDP.) You just fire off the current data and don’t worry about it because it’s changing 1/60th of a second later.

I’d recommend taking a look at your assumptions that led you to the idea that you need this kind of feedback at all from any function. There is certainly a case for some functions returning data. For example you want to refactor something out of a function for readability, or encapsulate the creation of an object.

So, I think this pattern is unnecessary, and as you mentioned, an anti-pattern for Godot and GDScript.

Makes my eyes fall out of my skull. This embodies everything that’s wrong with today’s approach to writing software.

But to give you the benefit of the doubt, let’s see some real world use cases where this could be actually beneficial. Let’s see a small game demo in which every function returns this object. Could it be shown that the whole thing ends up better for it, by an objective metric?

Btw. why are SUCESS, INFO and WARNING mutually exclusive? Why is this conceptualized as some kind of “severity scale”? A call could succeed and still return a warning and some info.

Sorry, I should have been more specific. This is for an editor plugin that creates and manages custom resources. So it’s not really for runtime stuff. It is mostly for input validation and disk write errors and such. You send a SSDMResult object to a custom UI component and it can color the message depending on the severity.

Btw. why are SUCESS, INFO and WARNING mutually exclusive? Why is this conceptualized as some kind of “severity scale”? A call could succeed and still return a warning and some info.

That’s what details are for. So you can return a success with warnings to be displayed in a UI. This way, execution continues because the function is a success but also shows warnings if something wasn’t what was expected. To be honest, I rarely use warning or info. It’s mostly pass or fail for me. I just left the other stuff in there because it isn’t hurting much and in case anyone else wanted to use it for other purposes.

Edit: Not that it changes anything. I was just clarifying.

As someone who has made multiple editor plugins, everything I said is still true.

If you want more specific feedback, you’d have to give us specific use cases. But again, you probably just don’t understand how Godot works.

Except Godot already has a fully functional UI for that stuff…