Hey ethan, great video. Have you used with any success or failure - pun intended - the first method you showed in the video with the two classes? Is that enough to cleanly handle error return types for most cases for try catch?
Thanks for watching! That method is a totally valid approach- it just ends up looking a lot like go and I don’t really love go. So for me Effect is really cool and worth using. But use the solution that makes the most sense for you
@@ethanniser Haha no wonder it was the solution that attracted me the most, Im quite fond of go. I like Effect too so ill definitely try them out at work and see how the team reacts. Thanks again, you earned yourself a subscriber!
Realistically, this only works as long as it's a language construct. Otherwise you expect an entire system to be written this way which is not the case.
I would disagree- haskell exists and you can use it but no one does effect is uniquely interesting because its not a separate language, but instead brings so many new features to typescript natively it allows easy interop between non effect code which means you can just write parts in effect and 'run' them to a promise at any time effect.website/docs/other/myths#effect-should-be-a-language-or-use-a-different-language
@@ethanniser Strange argument in the context of this video. People don't use Haskell purely for the error handling. They use JavaScript because it provides a different surface than, say, Haskell does, without the need to go around avenues to get interoperability. 'The right tool for the job' and all that, but it's more nuanced than this. Also to rely on Promise as the construct for dealing with Effect is odd and adds additional overhead for no other reason than interop. That overhead may seem minuscule at first, until the code are in hot paths. Been there with other libraries that aims to solve the same problem, and it just doesn't scale beyond a certain point unless you go all-in.
The way I see it, exceptions should only be used as a flow control mechanism and if I throw an exception I don't expect it to be handled anywhere near where the exception was thrown. Ideally the whole program or the thread, function, etc. just logs some error and terminates. I can then release a bugfix and make sure that exception simply isn't thrown again. A catch statement should be used only sparingly and with great consideration. If an operation is allowed to fail (e.g. saving a file, making a http request), then it should return a boolean/number value so that it can be easily checked, while the actual result would be provided as out parameter. Where it makes sense, it is also fine to return the value directly with error being indicated using a null or undefined (depending on language convention). In such case the error should be handled right there in some way (retry, use alternate method,...) or an exception can be generated to terminate the relevant block of code. This should be the primary way of dealing with failure. I don't think that writing code in such a way that multitudes of failures may cascade up several levels is a good design pattern.
JavaScript doesn't have the concept of `out` parameters, although you could mimic it with objects by mutating the object, but you'd need to create runtime guarantees as you can't just assign the variable like `out`. It's just not a common (if at all used) pattern outside of the 'C' languages.
@@dealloc Yeah, luckily, I find that in most of my js code, I don't really need to repeat an operation if it fails. JavaScript is, for lack of a better word, very "robust" where most operations can either be made to never fail, or they can be allowed to just completely terminate.
Me knowing I have a white Zed sweatshirt and a pristine white/cream background in my office about to be like I must be the counterbalance to the dark side.
1. yes the idea is that you use effect everywhere 2. effects decolors sync vs async 3. effect.website/docs/other/myths#effect-will-make-your-code-500x-slower
@@ethanniser I feel like you can solve so many of these issues by just using errors as values instead of throwing. why introduce all this complexity of mental and syntax overhead for leaning effect when you can just return the error instead of throwing? that automatically solves the error type issues and natively works with TS. the advantages of effect seem dubious for error handing to me. there are also lots of times when you can't use effect like if you are in a large codebase or dont have control over the whole code so the coloring problem is a significant blocker imho. in terms of the async handing stuff like fetching (just going to the effect home page examples), I feel like react query solves these issues in a much more intuitive manner and is much more easy to reason about. but of course that only works for react applications and its scope is significantly more limited
as I showed in the video, you don't have to use effect to get typesafe errors- effect just makes things really nice to work with (and also does a bunch of other stuff besides just typed errors) effect makes it very easy to interop with non effect code, you can 'run' any effect to a promise and await it in a non effect function effect is in a completely different category to react query. effect is a set of primitives for writing typescript which can be used anywhere, where react query is very specific to frontend async state management
3 месяца назад+1
@@bean_TM That's a very common thought, and I had the same question in the past. The problem is that it would require you to litter your code with checks for this and that error type, imagine how many ifs. It would be similar to what he did at 4:00 in the video. The ability to chain data transformation/handling using map/flatMap, or yield for a more imperative style, provides a much better developer experience!
Great video, but I wanted to note that I personally felt a lack of some code highlighting especially at the moment where you shows success and failure classes, also even though you mentioned that this is the fastest way of declaring both type and value, I feel like most of the Typescript devs not so familiar with this pattern, and I think it would be better to take some time for explaining this approach in more details. Great video anyways
Hi Ethan , How are you ? Did you learn DSA (trees , graph, heaps, sorting algorithms, searching algorithms etc) for becoming a better software engineer?
I was part of a competitive coding club in high school and took DSA at a local college but I would describe my DSA knowledge as unremarkable will have to probably learn some more for college though
I'm not Ethan, but those things "help" but what's best is to solve real problems you're having and building your own projects. Build a command line or web app that will help you solve something. I'm doing language learning so I pulled vocabulary review and have it test me like my own Duolingo. Do things that interest you. DSA are important and review a few videos periodically but like, they're not intrinsically motivating as doing your own thing.
3:45 I m curious why would we trade try/catch for this ...try catch needs to verify error type in runtime with "instanceof" to distinguish error type, but this Result needs to do that every single time, even if nothing errors. So we are basically losing performance and even make our code more complex with extra layer of wrap
I have the same idea as well, I'm just wondering if we create many class instances or pass lots of callback every single time, could that somewhat reduce your app performance?
@@kodicraft i for sure understand the reason behind this, its even said in this video ...just talking about performance, the question is if this steals performance with that check and I would pressume it does
is it objectively slower- yes but we are talking like nano seconds if the reason your app is slow is because of checking the output of a result type I don't think you should be using javascript but even then this pattern is literally what faster languages like go and rust do...
@@ethanniser i understand the runtime speed difference between interpreted and compiled languages, or even compiled with runtime costs (like golang gc) ...I am aware of these tradeoffs, what interests me is that I always thought that "instanceof" is something that is slow, its not regular "if number is greater than 10" condition. I cant justify this, i ve just heard it a lot of times that when there is dynamic type and we check if its one of those possibilities, that is slow. Maybe that isnt the case and i would like to understand the cost of it ....maybe it even turns out that catching exception is even slower and using that mechanism in js is far worse than matching instance type. I recently heard that in go passing "any" type to function and matching its type "using var.(type)" has big runtime cost
This is how we leverage TS to scale teams. Great stuff 👏 These patterns pay dividends and have stable, compounding benefits due to their underlying compositional definitions, much like React Components, building with Effect starts to unlock massive wins in the long term. It's the same kind of thing you hear people talk about with Elm / F# / OCaml etc. The compiler has your back.
I agree with most of what Ethan says; though I don't think the solution is introducing library-level dependencies for handling errors. Beside it adding additional overhead in both axis (dependencies _and_ performance), there is no way to provide an idiomatic way to handle errors; it relies on some pattern that may seem idiomatic, but at the end of the day still relies on a library to provide any good ergonomics out of it. The core problem is that there is a lack for first-class syntax around exception handling. It's kind of crazy that we've got so many new syntax sugars and features for dealing with async (async/await/generators) and OO (class) code that heavily relies on exceptions as a control flow mechanism, yet we have seen no changes and progress in terms of solving the ergonomics around exceptions. The way we handle exceptions in JS today is the same as we did in the 90s when it was first created. Heck, even the pattern matching proposal is very lacking in its scope when it comes to this; the only mention of exception handling is additional syntactic sugar (`catch match`), which doesn't actually solve the underlying problem-it's no different than putting the match clause inside the catch scope, other than removing the need for a few braces. I think we could learn from other languages that deal with exceptions and provides syntax to deal with them; like Swift's guards, if let, and option (??) to deal with both 'throwable' code, as well as code dealing with Result and Option-type code in the same manner. I am not saying we should introduce 'bonads' in JavaScript, though. Apart from that, there is an additional problem with relying on checked exceptions; that was also brought up by Ryan Cavanaugh in some of the threads on the subject; that checked exceptions adds API surface which are hard to scale, similarly to having a callback-style pattern to deal with multiple "error types", like (noFileError, otherError, ..., fileHandle) => {...} it adds a ton of noise (apart from the overhead) and makes APIs unstable since they could change at anytime. The only 'solution' we have right now is NodeJS-style callbacks (err, result) where err can be discriminated through a `type` field. But this also has negative ergonomics around control flow within and outside the scope of these functions (return/continue/throw).
Honestly, it’s kinda difficult to comprehend those examples. I think it’s partly because I rarely work with classes these days and don’t have enough experience with TypeScript generic types. Anyway, sounds useful
i think this complicates it more i think just returning the error is fine class DividedByZeroError extends Error { override readonly message = "DividedByZeroError"; } function divide(a: number, b: number) { if (!b) { return new DividedByZeroError(); } return a / b; } function divideAndDouble(a: number, b: number) { const result = divide(a, b); if (result instanceof Error) { return result; } return result * 2; } divideAndDouble(random(0, 9), random(0, 9)); // ^returns: number | DividedByZeroError
also you can do this with promises function errorify(value: unknown): T function errorify(value: unknown): Error function errorify(value: unknown) { if (value instanceof Error) { return value } return new Error(String(value)) } fetch("...").catch(errorify);
the problem with this is there is no consistent way to determine success from error, so implementing a map or flatMap like I showed in the video is not possible
You'd have to handle every possible error in every call of `divideAndDouble` all the way from `divide` and down since otherwise you'd be operating on every possible error type thrown throughout the chain of calls. This is much less ergonomic than try/catch which has the benefit (and downsides) of being a separate control flow-I know because I tried it, and it doesn't scale; even with utility functions to abstract it further, but then you end up with custom Result type anyway.
The expected/unexpected errors thing is dumb. Exceptions don't just magically throw themselves, functions are explicitly implemented in a way where they throw exceptions. If MDN can tell me which functions may throw which exceptions, why can't typescript? Effect is giving you incredibly ugly syntax to do something that should've just been a language feature, there's really nothing else to it
The functions on mdn are tightly controlled, specification defined functions- of course they have precise documentation on errors The js ecosystem is built on its community packages- most of which have no where near this level of documentation around their errors Having mdn equivalent precision doesn’t just require a library to declare what it throws, but every dependency it uses as well. Because this isn’t like a return type where the library controls what gets returned- any external function call in that library could throw and be exposed. And even if they did there is still a place for a untyped “panic” (see as rust and go libraries use these commonly) so no matter what most of the catch types still end up as “unknown | T” which as I show in the video is just not that useful on a type level You can get the same outcomes with “@throws” jsdoc + some lint rules to make sure you check or redeclare
@@ethanniser before typescript I would've made the same argument about ultra dynamic js code; it's too complex to type accurately. But typescript has not only enabled us to model incredibly complex API's, it also lets you do the vast majority through inference. Why would typed exceptions be hard to implement? Just DFS down every function called by a function and accumulate all the exceptions they might throw. If inference is slow, use editor tooling to add the throws annotation (just like you can already do for params or return types) And yeah libraries would probably need to add annotations, but they also needed to write types in the first place when typescript came out which is way more work. I do agree "panics" are good in the rare cases you actually want an unrecoverable error. But exceptions are recoverable, they shouldn't be used to implement a panic function. So as long as you only use exceptions for recoverable errors you've essentially got Result but with less noisy syntax.
Unfortunately, what you are describing is just not possible See this lengthy and detailed break down from Ryan Cavanaugh (dev lead for the ts team) github.com/microsoft/TypeScript/issues/13219#issuecomment-1515037604
its more about how can we increase safety having the expected errors as part of the return type is safer than hoping you remember to catch some unknown thrown error
aint no way theo is holding you hostage at his studio... blink twice if you are in danger... we can get you help
😉😉
exactly my thoughts
Theo looks way better with dark brown hair
It's funny because his last name is Browne
theo finally got rid of his mustache
what in the t3
What have you done with Theo
XDD
This is the best version of Theo. You’re thee Theo now.
Hey ethan, great video. Have you used with any success or failure - pun intended - the first method you showed in the video with the two classes? Is that enough to cleanly handle error return types for most cases for try catch?
Thanks for watching!
That method is a totally valid approach- it just ends up looking a lot like go and I don’t really love go.
So for me Effect is really cool and worth using. But use the solution that makes the most sense for you
@@ethanniser Haha no wonder it was the solution that attracted me the most, Im quite fond of go. I like Effect too so ill definitely try them out at work and see how the team reacts.
Thanks again, you earned yourself a subscriber!
Realistically, this only works as long as it's a language construct. Otherwise you expect an entire system to be written this way which is not the case.
I would disagree- haskell exists and you can use it but no one does
effect is uniquely interesting because its not a separate language, but instead brings so many new features to typescript natively
it allows easy interop between non effect code which means you can just write parts in effect and 'run' them to a promise at any time
effect.website/docs/other/myths#effect-should-be-a-language-or-use-a-different-language
@@ethanniser Strange argument in the context of this video. People don't use Haskell purely for the error handling. They use JavaScript because it provides a different surface than, say, Haskell does, without the need to go around avenues to get interoperability.
'The right tool for the job' and all that, but it's more nuanced than this. Also to rely on Promise as the construct for dealing with Effect is odd and adds additional overhead for no other reason than interop. That overhead may seem minuscule at first, until the code are in hot paths. Been there with other libraries that aims to solve the same problem, and it just doesn't scale beyond a certain point unless you go all-in.
The way I see it, exceptions should only be used as a flow control mechanism and if I throw an exception I don't expect it to be handled anywhere near where the exception was thrown. Ideally the whole program or the thread, function, etc. just logs some error and terminates. I can then release a bugfix and make sure that exception simply isn't thrown again. A catch statement should be used only sparingly and with great consideration.
If an operation is allowed to fail (e.g. saving a file, making a http request), then it should return a boolean/number value so that it can be easily checked, while the actual result would be provided as out parameter. Where it makes sense, it is also fine to return the value directly with error being indicated using a null or undefined (depending on language convention).
In such case the error should be handled right there in some way (retry, use alternate method,...) or an exception can be generated to terminate the relevant block of code. This should be the primary way of dealing with failure.
I don't think that writing code in such a way that multitudes of failures may cascade up several levels is a good design pattern.
JavaScript doesn't have the concept of `out` parameters, although you could mimic it with objects by mutating the object, but you'd need to create runtime guarantees as you can't just assign the variable like `out`. It's just not a common (if at all used) pattern outside of the 'C' languages.
@@dealloc Yeah, luckily, I find that in most of my js code, I don't really need to repeat an operation if it fails. JavaScript is, for lack of a better word, very "robust" where most operations can either be made to never fail, or they can be allowed to just completely terminate.
Me knowing I have a white Zed sweatshirt and a pristine white/cream background in my office about to be like I must be the counterbalance to the dark side.
You lost me until you showed that Effect actually infers all the errors. Woha, this is game changler.
wouldnt effect just "color" all the result types? which forces you to use effect everywhere? this also adds runtime overhead doesnt it?
1. yes the idea is that you use effect everywhere
2. effects decolors sync vs async
3. effect.website/docs/other/myths#effect-will-make-your-code-500x-slower
@@ethanniser I feel like you can solve so many of these issues by just using errors as values instead of throwing. why introduce all this complexity of mental and syntax overhead for leaning effect when you can just return the error instead of throwing? that automatically solves the error type issues and natively works with TS.
the advantages of effect seem dubious for error handing to me.
there are also lots of times when you can't use effect like if you are in a large codebase or dont have control over the whole code so the coloring problem is a significant blocker imho.
in terms of the async handing stuff like fetching (just going to the effect home page examples), I feel like react query solves these issues in a much more intuitive manner and is much more easy to reason about. but of course that only works for react applications and its scope is significantly more limited
as I showed in the video, you don't have to use effect to get typesafe errors- effect just makes things really nice to work with (and also does a bunch of other stuff besides just typed errors)
effect makes it very easy to interop with non effect code, you can 'run' any effect to a promise and await it in a non effect function
effect is in a completely different category to react query. effect is a set of primitives for writing typescript which can be used anywhere, where react query is very specific to frontend async state management
@@bean_TM That's a very common thought, and I had the same question in the past. The problem is that it would require you to litter your code with checks for this and that error type, imagine how many ifs. It would be similar to what he did at 4:00 in the video. The ability to chain data transformation/handling using map/flatMap, or yield for a more imperative style, provides a much better developer experience!
Your last two videos have been great! Hope you keep making these ❤
Great video, but I wanted to note that I personally felt a lack of some code highlighting especially at the moment where you shows success and failure classes, also even though you mentioned that this is the fastest way of declaring both type and value, I feel like most of the Typescript devs not so familiar with this pattern, and I think it would be better to take some time for explaining this approach in more details. Great video anyways
With the update of the effect, it seems that the effect workshop has become somewhat outdated. Is there a plan to update the effect workshop?
the concepts are all still the same, so I think the workshop is still a good resource
Hi Ethan ,
How are you ?
Did you learn DSA (trees , graph, heaps, sorting algorithms, searching algorithms etc) for becoming a better software engineer?
I was part of a competitive coding club in high school and took DSA at a local college but I would describe my DSA knowledge as unremarkable
will have to probably learn some more for college though
I'm not Ethan, but those things "help" but what's best is to solve real problems you're having and building your own projects.
Build a command line or web app that will help you solve something. I'm doing language learning so I pulled vocabulary review and have it test me like my own Duolingo.
Do things that interest you.
DSA are important and review a few videos periodically but like, they're not intrinsically motivating as doing your own thing.
thanks
I like this theo clone,
Keep up the good work mate
3:45
I m curious why would we trade try/catch for this ...try catch needs to verify error type in runtime with "instanceof" to distinguish error type, but this Result needs to do that every single time, even if nothing errors. So we are basically losing performance and even make our code more complex with extra layer of wrap
Well the point is that this forces you to gracefully handle the error case rather than pile it along with exceptions unrelated to program logic
I have the same idea as well, I'm just wondering if we create many class instances or pass lots of callback every single time, could that somewhat reduce your app performance?
@@kodicraft i for sure understand the reason behind this, its even said in this video ...just talking about performance, the question is if this steals performance with that check and I would pressume it does
is it objectively slower- yes
but we are talking like nano seconds
if the reason your app is slow is because of checking the output of a result type I don't think you should be using javascript
but even then this pattern is literally what faster languages like go and rust do...
@@ethanniser i understand the runtime speed difference between interpreted and compiled languages, or even compiled with runtime costs (like golang gc) ...I am aware of these tradeoffs, what interests me is that I always thought that "instanceof" is something that is slow, its not regular "if number is greater than 10" condition. I cant justify this, i ve just heard it a lot of times that when there is dynamic type and we check if its one of those possibilities, that is slow. Maybe that isnt the case and i would like to understand the cost of it ....maybe it even turns out that catching exception is even slower and using that mechanism in js is far worse than matching instance type. I recently heard that in go passing "any" type to function and matching its type "using var.(type)" has big runtime cost
Hi t3dotgg 2.0
theo is that you?!
This is how we leverage TS to scale teams. Great stuff 👏
These patterns pay dividends and have stable, compounding benefits due to their underlying compositional definitions, much like React Components, building with Effect starts to unlock massive wins in the long term. It's the same kind of thing you hear people talk about with Elm / F# / OCaml etc. The compiler has your back.
I agree with most of what Ethan says; though I don't think the solution is introducing library-level dependencies for handling errors. Beside it adding additional overhead in both axis (dependencies _and_ performance), there is no way to provide an idiomatic way to handle errors; it relies on some pattern that may seem idiomatic, but at the end of the day still relies on a library to provide any good ergonomics out of it.
The core problem is that there is a lack for first-class syntax around exception handling. It's kind of crazy that we've got so many new syntax sugars and features for dealing with async (async/await/generators) and OO (class) code that heavily relies on exceptions as a control flow mechanism, yet we have seen no changes and progress in terms of solving the ergonomics around exceptions. The way we handle exceptions in JS today is the same as we did in the 90s when it was first created.
Heck, even the pattern matching proposal is very lacking in its scope when it comes to this; the only mention of exception handling is additional syntactic sugar (`catch match`), which doesn't actually solve the underlying problem-it's no different than putting the match clause inside the catch scope, other than removing the need for a few braces.
I think we could learn from other languages that deal with exceptions and provides syntax to deal with them; like Swift's guards, if let, and option (??) to deal with both 'throwable' code, as well as code dealing with Result and Option-type code in the same manner. I am not saying we should introduce 'bonads' in JavaScript, though.
Apart from that, there is an additional problem with relying on checked exceptions; that was also brought up by Ryan Cavanaugh in some of the threads on the subject; that checked exceptions adds API surface which are hard to scale, similarly to having a callback-style pattern to deal with multiple "error types", like (noFileError, otherError, ..., fileHandle) => {...} it adds a ton of noise (apart from the overhead) and makes APIs unstable since they could change at anytime. The only 'solution' we have right now is NodeJS-style callbacks (err, result) where err can be discriminated through a `type` field. But this also has negative ergonomics around control flow within and outside the scope of these functions (return/continue/throw).
So basically, you end up handling erros in TS exactly the way you do it in rust.. Congratulations, ya'll just learnt how Result type works in rust.
Yes… exactly
It’s a good pattern, and the point is that it’s is possible in typescript
i was here before 15k just letting yall know
Not gonna lie I thought this guy was a Theo rip off til I checked comments. I now see it was intentional 😂
LMFAO WHAT IS THIS?
WHERE IS THEO
Honestly, it’s kinda difficult to comprehend those examples. I think it’s partly because I rarely work with classes these days and don’t have enough experience with TypeScript generic types. Anyway, sounds useful
IMPOSTER
i think this complicates it more
i think just returning the error is fine
class DividedByZeroError extends Error {
override readonly message = "DividedByZeroError";
}
function divide(a: number, b: number) {
if (!b) {
return new DividedByZeroError();
}
return a / b;
}
function divideAndDouble(a: number, b: number) {
const result = divide(a, b);
if (result instanceof Error) {
return result;
}
return result * 2;
}
divideAndDouble(random(0, 9), random(0, 9));
// ^returns: number | DividedByZeroError
also you can do this with promises
function errorify(value: unknown): T
function errorify(value: unknown): Error
function errorify(value: unknown) {
if (value instanceof Error) {
return value
}
return new Error(String(value))
}
fetch("...").catch(errorify);
the problem with this is there is no consistent way to determine success from error, so implementing a map or flatMap like I showed in the video is not possible
You'd have to handle every possible error in every call of `divideAndDouble` all the way from `divide` and down since otherwise you'd be operating on every possible error type thrown throughout the chain of calls. This is much less ergonomic than try/catch which has the benefit (and downsides) of being a separate control flow-I know because I tried it, and it doesn't scale; even with utility functions to abstract it further, but then you end up with custom Result type anyway.
The expected/unexpected errors thing is dumb. Exceptions don't just magically throw themselves, functions are explicitly implemented in a way where they throw exceptions. If MDN can tell me which functions may throw which exceptions, why can't typescript?
Effect is giving you incredibly ugly syntax to do something that should've just been a language feature, there's really nothing else to it
The functions on mdn are tightly controlled, specification defined functions- of course they have precise documentation on errors
The js ecosystem is built on its community packages- most of which have no where near this level of documentation around their errors
Having mdn equivalent precision doesn’t just require a library to declare what it throws, but every dependency it uses as well. Because this isn’t like a return type where the library controls what gets returned- any external function call in that library could throw and be exposed.
And even if they did there is still a place for a untyped “panic” (see as rust and go libraries use these commonly) so no matter what most of the catch types still end up as “unknown | T” which as I show in the video is just not that useful on a type level
You can get the same outcomes with “@throws” jsdoc + some lint rules to make sure you check or redeclare
@@ethanniser before typescript I would've made the same argument about ultra dynamic js code; it's too complex to type accurately. But typescript has not only enabled us to model incredibly complex API's, it also lets you do the vast majority through inference.
Why would typed exceptions be hard to implement? Just DFS down every function called by a function and accumulate all the exceptions they might throw. If inference is slow, use editor tooling to add the throws annotation (just like you can already do for params or return types)
And yeah libraries would probably need to add annotations, but they also needed to write types in the first place when typescript came out which is way more work.
I do agree "panics" are good in the rare cases you actually want an unrecoverable error. But exceptions are recoverable, they shouldn't be used to implement a panic function.
So as long as you only use exceptions for recoverable errors you've essentially got Result but with less noisy syntax.
Unfortunately, what you are describing is just not possible
See this lengthy and detailed break down from Ryan Cavanaugh (dev lead for the ts team)
github.com/microsoft/TypeScript/issues/13219#issuecomment-1515037604
zoomer theo?
I just don't get why people are so bothered by try/catch with if (instance of). it's not that annoying to write every time tbh
It's not about being "annoying" to work with.
The. Result pattern a well-known pattern for cleaner and more predictable code.
its more about how can we increase safety
having the expected errors as part of the return type is safer than hoping you remember to catch some unknown thrown error
Even PHP differentiates errors and exceptions.