I am having design doubts in Crystal and would like to hear how others have implemented similar functionality.
The User class has a method to fetch data based on some attribute value and return an instance.
class User
def self.get_by_login(login : String)
obj = DB.irrelevant_method(login)
return obj
end
end
(I am using superfluous return statements to be more illustrative of intent)
The issue comes in when the database does not find a record to return. I do not want to return a union type such as (User|Nil) or (User|Bool). My idea is to return a User instance to satisfy the return type with the specific error added to an existing errors array I'm already using for validation on the creation side.
class User
def self.get_by_login(login : String)
user = User.new
begin
user = DB.irrelevant_method(login)
rescue ex
user.errors << ex.to_s
end
return user
end
end
This leaves it up to the caller to check if the returned object is valid or has any errors before knowing what to do with it.
Any thoughts or concerns about this approach in Crystal or in general? I've used this pattern before in other languages and it does work, but I've never really felt great about it. I'm curious to hear how others have solved this or similar issues in Crystal or other strongly typed languages.







One thing to consider could be the security risks of having separate errors, if a developer doesn't know they have to check for errors or forgets, what will happen when an error occurs?
What some languages gain for free with optional/nullable/joint types is a crash or exception when the returned value is treated as a valid value. (Though this might make it less apparent where the error occured)
Others like go force you to explicitly ignore the error if you don't want to care about it.
Then there's those that would throw an exception at error, which would tell you immediately where the error is. Though this might make the code uglier, or make the developer lazy and just do a catch all.
I think some languages with union types force you to program different execution paths for each possible type, but that might be more exclusive to languages with inferred typing.