No matter how great we are at programming, sometimes our scripts have errors. They may occur because of our mistakes, an unexpected user input, an erroneous server response, and a thousand other reasons
Handling errors
There're some common pattern to handle errors
Returing null;
Throwing exceptions;
Returning exception;
The Option type
Returning null
function parse(birthday: string): Date | null
{
let date = new Date(birthday)
if (!isValid(date)) {
return null
}
return date;
}
// Checks if the given date is valid
function isValid(date: Date) {
return Object.prototype.toString.call(date) === '[object Date]'
&& !Number.isNaN(date.getTime())
}
}
Throwing exceptions
function parse(birthday: string): Date | null
{
let date = new Date(birthday)
if (!isValid(date)) {
throw new RangeError("Enter a date in the form YYYY/MM/DD");
}
return date;
}
try {
let date = parse(ask())
console.info('Date is', date.toISOString())
} catch (e) {
if (e instanceof RangeError)
{ console.error(e.message)
}
else { throw e; }
}
Returning error
TypeScript isn’t Java, and doesn’t support throws clauses.1 But we can achieve some‐ thing similar with union types
function parse( birthday: string): Date | InvalidDateFormatError | DateIsInTheFutureError
{
let date = new Date(birthday)
if (!isValid(date)) {
return new InvalidDateFormatError('Enter a date in the form YYYY/MM/DD')
}
if (date.getTime() > Date.now())
{
return new DateIsInTheFutureError('Are you a timelord?')
}
}
let result = parse(ask())
// Either a date or an error if
if (result instanceof InvalidDateFormatError) {
console.error(result.message)
} else if (result instanceof DateIsInTheFutureError)
{
console.info(result.message) }
else {
console.info('Date is', result.toISOString())
}
Encode likely exceptions in parse’s signature.
Communicate to consumers which specific exceptions might be thrown.
Force consumers to handle (or rethrow) each of the exceptions.
The try-catch syntax
try {
// code
} catch (err) {
// error handling
}
There's a diagram to show us how it work
try...catch
works synchronously
If an exception happens in "schedule" code, like in setTimeout, then try catch won't work
try {
setTimeout(function(){
noSuchVariable; // script will die here
},1000)
} catch(err) {
}
setTimeout(function() {
try {
noSuchVariable; // try...catch handles the error!
} catch {
alert( "error is caught here!" );
}
}, 1000);
Error object
When an error occurs, JavaScript generates an object containing the details about it. The object is then passed as an argument to catch
For all built-in errors, the error object has two main properties:
name (error name). For instance, an undefined variable that's "ReferenceError"
message (textual message for about error detail)
stack (current callstack; this shows us where the error happened)
try {
lalala; // error, variable is not defined!
} catch (err) {
alert(err.name); // ReferenceError
alert(err.message); // lalala is not defined
alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)
// Can also show an error as a whole
// The error is converted to string as "name: message"
alert(err); // ReferenceError: lalala is not defined
}
Throwing our own error
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
alert( user.name ); // no name!
} catch (err) {
alert( "doesn't execute" );
}
Build-in errors in JS
We have a few build-in constructors for standard errors:
Error
SyntaxError
ReferenceError
TypeError
URIError
RangeError
//Syntax Error
let x = 5;
console.log(x)
//Reference Error
console.log(variableDoesNotExist);
Uncaught ReferenceError: variableDoesNotExist is not defined
at <anonymous>:1:13
//Type Error
const obj = null;
null.undefined;
VM42:4 Uncaught TypeError: Cannot read properties of null
(reading 'property') at <anonymous>:4:17
//URI Error
decodeURIComponent('%');
VM42:1 Uncaught URIError: URI malformed
at decodeURIComponent (<anonymous>)
at <anonymous>:1:2
(anonymous) @ VM42:1
Of course, JS is not going to hand-feed us because the use-case support to cover every single scenario is impractical. If we want to expand this to handle some more special cases, we can extend the current error object.
Handle HTTP error from server to Error in JS
Design system
After we create our HTTPError, we can then use it to handle error for our request to server
export async function apiCall(path, init) {
let response;
let json;
try {
const jsonRespInfo = await getJSON(`/api/${path}`, init);
response = jsonRespInfo.response;
json = jsonRespInfo.json;
} catch (err) {
if (err instanceof HTTPError) throw err;
throw new Error(
stringifyError(
`Networking/apiCall: An error was encountered while making api call to ${path}`,
err,
),
);
}
if (!response.ok)
throw new HTTPError(response, 'Problem while making API call');
return json;
}
Handle error with unknown
try {
// technically we can throw anything
throw "new string";
} catch(err) {
// in this case because of string wrapper it will be undefined but
// you can mess it :)
err.stack;
}
try {
} catch(err:unknown) {
if(isError(err)) {
// best is to use type-guard to catch it
console.log(err);
}
}
Global catch
Let’s imagine we’ve got a fatal error outside of try...catch
, and the script died. Like a programming error or some other terrible thing. We can have another extra layer of protection in case we forget to handle it somehow
window.onerror = function(message,url,line,col,error) {
// something has happened
}
Centralize errors on ReactJS
class GlobalErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { error: error.stack, hasError: true };
}
componentDidCatch(error, errorInfo) {
console.warn(error, errorInfo);
const stack = error.stack;
let errorType = '';
if (stack === null) {
errorType = stack.substring(0, stack.indexOf(':'));
}
this.props.tracker(EVENT_TRACKING_JAVASCRIPT_ERROR, {
'Error message': error.message,
'Error stack': error.stack,
'Error type': errorType,
errorInfo
});
serverLoggerKibana.log(
this.props.api,
this.props.configuration,
getAppInfo(),
SERVER_LOGGER_REACT,
'error',
error,
errorInfo
);
}
render() {
//Render a component to show a pop-up error to users
}
Custom errors, extending Error
When we develop something, we often need our own error classes to reflect specific things that may go wrong in our tasks. For errors in network operations we may need HttpError
database operations DbError
, for searching operations NotFoundError
and so on.
Our errors should support basic error properties like message
, name
and, preferably, stack
. But they also may have other properties of their own, e.g. HttpError
objects may have a statusCode
property with a value like 404
or 403
or 500
.
Let's explore Error class
// The "pseudocode" for the built-in Error class defined by JavaScript itself
class Error {
constructor(message) {
this.message = message;
this.name = "Error"; // (different names for different built-in error classes)
this.stack = <call stack>;
}
Write our own ValidationError class to support validation error
class ValidationError extends Error {
constructor(message) {
super(message);
//override it
this.name = "ValidationError";
}
}
// Usage
function readUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new ValidationError("No field: age");
}
if (!user.name) {
throw new ValidationError("No field: name");
}
return user;
}
// Working example with try..catch
try {
let user = readUser('{ "age": 25 }');
} catch (err:unknown) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No field: name
} else if (err instanceof SyntaxError) { // (*)
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // unknown error, rethrow it (**)
}
}
Tasks
Create a class FormatError
that inherits from the built-in SyntaxError
class.
It should support message
, name
and stack
properties.
class FormatError extends SyntaxError {
// we have to pass in message to super
// and write our own constructor name
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
let err = new FormatError("formatting error");
alert( err.message ); // formatting error
alert( err.name ); // FormatError
alert( err.stack ); // stack
alert( err instanceof FormatError ); // true
alert( err instanceof SyntaxError ); // true (because inherits from SyntaxError)
Summary
We can inherit from
Error
and other built-in error classes normally. We just need to take care of thename
property and don’t forget to callsuper
.We can use
instanceof
to check for particular errors. It also works with inheritance. But sometimes we have an error object coming from a 3rd-party library and there’s no easy way to get its class. Thenname
property can be used for such checks.Wrapping exceptions is a widespread technique: a function handles low-level exceptions and creates higher-level errors instead of various low-level ones. Low-level exceptions sometimes become properties of that object like
err.cause
in the examples above, but that’s not strictly required.