Error handling practice - OfficeDev/TeamsFx GitHub Wiki
UserError
and SystemError
How to classify UserError: errors caused by user's input/behavior, which can fixed by user's proper behavior. For example:
- User's subscription has no permission on some resources
- Invalid inputs
- Concurrent operation error, please wait until the running task finishs or you can reload the window to cancel it.
SystemError: errors caused by system bugs or failures, which can not fixed by user. For example,
- Cannot read property ' [*] ' of undefined
For Errors thrown by third-party modules, it's better to analyze and classify the Error types:
failed to exec dotnet script.,errorStack=Failed to install .NET Core SDK (v3.1). Install .NET Core SDK (v3.1) manually and restart Visual Studio Code
Frequent UserError means that user experience of the product should be improved to decrease the possibility of user using the toolkit in the wrong way. Frequent SysteError means that the system should reduce bugs to improve the quality and increase the usability.
How to define error
Three basic method to define error
There are many ways to define customized errors in the project:
- Define an error as a constant value
- Define an error as a function that returns a
UserError
orSystemError
- Define an customized error class
- directly construct a
UserError
orSystemError
in the place where error happens- construct error (
UserError
/SystemError
) with name, message,source,... - construct error (
UserError
/SystemError
) with existing Error object - construct error (
UserError
/SystemError
) with option (UserErrorOptions
/SystemErrorOptions
) object
- construct error (
We don't suggest first method, unless you don't care about the stack at all. Because the error stack is constant and meaningless in such a case.
We don't suggest the second method neither, which add one more call stack on top of the stacks where the error really happens.
Here is a bad sample:
function MyError() {
return new UserError("MyError", "my error message", "API");
}
console.log(MyError());
The error stack will contains the function MyError
, which is not expected:
We suggest the third method to write your own error class that extends UserError
or SystemError
.
For example:
class MyError extends UserError {
constructor () {
super(new.target.name, "my error message", "API")
}
}
Then the stack will be clean:
Another advantage of the third method is that it support instanceof
operation on error type checking:
console.log(new MyError() instanceof MyError); // output `true`
Because in the constructor, we have passed new.target.name
(which is actually MyError
) as error name parameter into the super
constructor.
Alternatively, if we don't want to define an error name different from the class name, we can just pass an empty string as the error name.
The following definition is equivalent to previous one:
class MyError extends UserError {
constructor () {
super("", "my error message", "API")
}
}
The reason is the in constructor of UserError
/SystemError
, we have some priority checking on error name. Explicitly input name is the first priority, then the name of Error
object is the second priority. If both are empty, we will use the constructor name as the default error name:
this.name = option.name || (option.error && option.error.name) || new.target.name;
In addition to method three, We also suggest the fourth method, because it is the most flexible.
Construct a UserError/SystemError
We have three override constructors for UserError
or SystemError
.
name
, message
and source
string
Constructor with const error = new UserError(myName, myMessage, mySource, undefined, myHelpLink);
chai.assert.equal(error.name, myName);
chai.assert.equal(error.message, myMessage);
chai.assert.equal(error.source, mySource);
chai.assert.equal(error.helpLink, myHelpLink);
chai.assert.isDefined(error.timestamp);
chai.assert.isTrue(error instanceof UserError);
Error
object
Constructor with existing const innerError = new RangeError(myMessage);
const error = new UserError(innerError, mySource);
chai.assert.equal(error.name, "RangeError");
chai.assert.equal(error.message, myMessage);
chai.assert.equal(error.source, mySource);
chai.assert.isTrue(error instanceof UserError);
chai.assert.equal(error.innerError, innerError);
When constructing with existing Error object, the name of the input Error will have higher priority than the constructor name.
UserErrorOptions
/SystemErrorOptions
) object
Constructor with an option (This approach provides most flexible way to construct a Error:
const error = new UserError({ error: new RangeError(myMessage), source: mySource, helpLink: myHelpLink });
chai.assert.equal(error.name, "RangeError");
chai.assert.equal(error.message, myMessage);
chai.assert.equal(error.source, mySource);
chai.assert.equal(error.helpLink, myHelpLink);
chai.assert.isTrue(error instanceof UserError);
You can input an empty option to build an error, the constructor will replace empty values with some default values:
const error = new UserError({});
chai.assert.equal(error.name, "UserError");
chai.assert.equal(error.message, "");
chai.assert.equal(error.source, "unknown");
chai.assert.isTrue(error instanceof UserError);