I am working on a complex application with different types of errors, services, and domain concepts.
In order to throw “object” errors, there are two different approaches that come to my mind:
- Applying
Object.assign()
to an Error object (easy option if I just need to throw a single or a couple of errors that follows this form):
function f() { const err = new Error(); Object.assign(err, { name: "ServiceError", code: "service/some-string-code", message: "Some message", }); throw err; } try { f(); } catch(err) { console.log(err instanceof Error); }
- Creating custom errors (extending the Error class)
class MyServiceError extends Error { constructor(code, message) { super(message); this.name = "ServiceError"; this.code = code; } } function f() { const err = new MyServiceError("service/some-string-code", "Some message"); throw err; } try { f(); } catch(err) { console.log(err instanceof Error); console.log(err instanceof MyServiceError); }
What are the pros and cons between both “custom errors definitions”.
Also, if I choose the second approach, it seems that I will need to create multiple CustomError
classes for different domain concepts, services, etc. in order to achieve symmetric code, and a clean architecture… (???) Which in turn I see as reinventing the wheel and adding unnecessary code, because perhaps not all the concepts of the application should need a custom type of exception.
Are both practices considered valid in JavaScript?
Note: throwing objects or string or stuff like that seems really bad to me, as we are not able to get the stack trace, validate instances etc.
// This seems bad to me. Isn't it an anti-pattern? throw { code: "", message: "", name: "" }
Advertisement
Answer
The Object.assign approach is less robust and more of a hack, it is better to create custom error class. There is already an in-depth discussion on SO.
As you want to use additional fields, at most, introduce 2-3 custom classes for internal errors, but even that is often overkill:
- one for
NetworkError
with location, path and status - one for
UiError
with component and problematic data state and maybe a message code for i18n - and one generic
RuntimeError
, or similar, for unknown cases
It makes little sense to have an error class per each potential occurrence. Unlike Java, there are no checked exceptions in JavaScript, and the goal is to have just enough data to troubleshoot the problem, without over engineering it. If you can meaningfully capture and then display in a dialog more data than message
string would hold, go for it.
When designing custom errors, start with where and how you will process and show this information. Then see if you can easily collect this data where you throw it. If you do not have global error dialog or centralized error reporting, maybe just the default Error is enough, and you can place all the data into the message.
There is one special case, when you want to use errors as means of controlling the logic. Try to avoid it as much as possible, JavaScript is very flexible to not use throw
as a way to let upper layer choose a different execution path. However, it is sometimes used to re-try network requests, and then it should have enough data for that.
Built-in Error object already has following fields:
- name
- message
- stack
In each error, stack
and message
are two crucial pieces of information helping to fix the problem. Therefore it is important, when you re-throw it, to use something like this (for everything non-IE):
catch (err) { throw new Error('New error message with added info', { cause: err }); }
Last, it helps to check what others are doing:
- GraphQL’s GraphQLError
- Error handling hooks in VueJS (it has no custom errors)
And, JavaScript has not just Error
, but also:
- EvalError
- RangeError
- ReferenceError
- SyntaxError
- TypeError
- URIError
- AggregateError
You can also throw them when appropriate.
Note, that most UI frameworks handling views do not have custom error classes, nor do they need one.