When to return a Result.Error and when to throw an Exception?
Patrick Roza
Posted on June 12, 2019
In the previous part I introduced the Result class as a means of control flow and error handling. Now I will focus on when to use the Result class to return an Error, and when to throw instead.
There appears to be quite some different opinions on the subject:
- Always throw an Error
- Always throw errors and then only wrap them when you want to handle them
- Only return recoverable errors, throw the rest
- Return expectable errors, throw (or pass through) the rest.
- Return every error, never throw. (Wrap any library error)
I am mostly in Camp 4, although I think 3 and 4 actually mean the same thing, most of the time. As always, it depends on the use case.
I would generally return: Domain errors and certain Infrastructure errors
- SoldOut, RoomAlreadyBooked, PaperJammed, InvalidProductCode, CodeAlreadyRedeemed, PaymentMethodFailed
- ValidationError, InvalidStateError, ForbiddenError, AuthenticationRequiredError
- APIError/DBError: RecordNotFound, RecordAlreadyExists, (Optimistic)LockError, CouldNotAcquireLockError, ConnectionError, RemoteServiceError
I would throw exceptions (panics):
Programmer errors:
- ArgumentException
- NullException
- DivideByZeroException
Corruption errors, e.g: SerializationException
- The runtime will also throw various errors, for instance in case of StackOverflow or OutOfMemory.Such errors should abandon the current flow, and raise an exception, probably caught at the highest level, usually for error logging.
In the end, it depends on what you're building.
Catch & Wrap ‘all the tings’?
Does this mean you should catch and wrap all third party errors? No, it follows the same principals; if it falls into expectable errors like one that represents api call status codes like 400 or 404, or e.g a database record not found; wrap them.
Why return instead of throw
a technique that allows you to capture errors elegantly, without contaminating your code with ugly conditionals and try/ catch statements.
From the book “Domain modeling made functional”; A great read, really recommended!
- try/catch/finally is verbose, indenting/nesting at least 1 scope deep, often also leading to multiple levels deep (within 1 or more functions)
- Errors become part of the return type signature; automatic documentation, and compiler assistance when missing to handle one
- It enables composability
- Throwing errors is expensive (stack traces etc), while for the error cases you would return, generally I see no use for stack traces. Now I’ll be the first to say: don’t prematurely optimize, but if you can design your application from the ground up to be performant, even in the expectable error cases; that’s great.
As a side note, in javascript:
- There is no ‘beautiful’ built-in way to handle different error types differently, other than
if
statements orswitch
, unlike e.g in C# or Python. - There is no ‘beautiful’ built-in way to document the errors that may occur, and have type-safety for them, unlike in e.g Java.
Other examples of implementations that favor return status over Exceptions
-
fetch : The fetch implementation will only throw on a Network Error. It will use an
ok
Boolean that will be false on a status code > 400. It isn’t as elegant as the Result class though. -
Promise : once resolved or rejected; either success or error. Has the benefit of
await
support however.
Relationship to "Checked" Exceptions
https://stackoverflow.com/questions/613954/the-case-against-checked-exceptions
https://softwareengineering.stackexchange.com/questions/150837/maybe-monad-vs-exceptions
Perhaps summated as:
With great power comes great responsibility
I think the difference is that Checked exceptions are a forced concept , but Result can be an application level decision you make, you opt-in.
In fact, if you don't want to handle a Result.Error, you can just let it bubble up, there's no requirement to handle it, unless you explicitly decide "this is the place I want to handle each and every exception in a certain way", and e.g use Typescript's "exhaustive switch block" or similar.
Finally, in Typescript and F# there are Union types that help you "fold" errors into higher order ones, unlike with Checked exceptions in e.g Java.
Violation of the Open/Closed Principle?
https://stackoverflow.com/questions/54882275/do-checked-exceptions-violate-the-open-closed-principle
OCP appears to apply to modules, not methods. Also I think that if you add a new exception, you are more "silently" breaking the contract. At least Return error types don't lie ;-)
Error handling samples
Handle Success, or Error case
doSomething()
.pipe(match(
value => ctx.body = value,
err => ctx.statusCode = 500,
))
Fallback to a default value
const result = doSomething()
.pipe(orDefault(“some-default-value”))
// if doSomething() fails, use “some-default-value” instead.
High level REST api error handler (more elegant ideas welcome!)
if (err instanceof RecordNotFound) {
ctx.body = { message }
ctx.status = 404
} else if (err instanceof CombinedValidationError) {
const { errors } = err
ctx.body = {
fields: combineErrors(errors),
message,
}
ctx.status = 400
} else if (err instanceof FieldValidationError) {
ctx.body = {
fields: { [err.fieldName]: err.error instanceof CombinedValidationError ? combineErrors(err.error.errors) : err.message },
message,
}
ctx.status = 400
} else if (err instanceof ValidationError) {
ctx.body = { message }
ctx.status = 400
} else if (err instanceof InvalidStateError) {
ctx.body = { message }
ctx.status = 422
} else if (err instanceof ForbiddenError) {
ctx.body = { message }
ctx.status = 403
} else if (err instanceof OptimisticLockError) {
ctx.status = 409
} else if (err instanceof CouldNotAquireDbLockError) {
ctx.status = 503
} else if (err instanceof ConnectionError) {
ctx.status = 504
} else {
// Unknown error
ctx.status = 500
}
Which would be accompanied by a catch-all exception handler for status 500, and logging:
try {
// execute the workflow
// handle success and error result cases
} catch (err) {
logger.error(err)
ctx.status = 500
}
Source
As always you can also find the full framework and sample source at patroza/fp-app-framework
Other reads
What's Next
Next in the series, I plan to explore my ideal vision of the future, once we receive pipe operator |>
and improved Generator typing support in Typescript ;-)
Posted on June 12, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.