Deno’s built-in errors

Mayank Choubey
Tech Tonic
Published in
5 min readDec 14, 2021

--

Introduction

All applications need to raise and handle errors. Generally, real-world applications has numerous function calls spanning over a number of ES modules, external services, etc. This makes error handling a fundamental job.

There are two common ways to return errors:

  • Return a composite object of err & data where, err has a code/object if the function raised an error, null otherwise
  • Throw an exception

Return composite object

The function can return a composite object, always containing two pieces of information:

  • err: An error code or error object if error is present, null otherwise
  • data: The data in case of successful execution
// someModule
async function someFunction(body: string) {
if (missingData()) {
return { err: someCode, data: null };
}
//get some data
return { err: null, data };
}
async function someOtherFunction(body: string) {
if (notAccessible()) {
return { err: someOtherCode, data: null };
}
//get some data
return { err: null, data };
}
//main module
async function reqHandler(req: Request) {
const { err, data } = someFunction(request.body);
if (err && err === someCode) {
return new Response(null, { status: 400 });
}
const { err2, data2 } = someOtherFunction(request.body);
if (err2 && err2 === someOtherCode) {
return new Response(null, { status: 403 });
}
//do something with data
return new Response(newData);
}

The only problem with this approach is to check error codes after every call.

Throw exception

The function can throw appropriate exceptions that the caller can catch and act upon. The following is the same code, but rewritten with exceptions. A new error type (Inaccessible) is defined to cover the case when some resource is inaccessible.

// someModule
class Inaccessible extends Error {
constructor(msg) {
super(msg);
this.name = "Inaccessible";
}
}
async function someFunction(body: string) {
if (missingData()) {
throw new TypeError("Missing mandatory data");
}
//get some data
return data;
}
async function someOtherFunction(body: string) {
if (notAccessible()) {
throw new Inaccessible("Missing mandatory data");
}
//get some data
return data;
}
//main module
async function reqHandler(req: Request) {
let newData;
try {
const data = someFunction(request.body);
const data2 = someOtherFunction(request.body);
//do something with data
} catch (err) {
if (err instanceof TypeError) {
return new Response(null, { status: 400 });
} else if (err instanceof Inaccessible) {
return new Response(null, { status: 403 });
} else {
return new Response(null, { status: 500 });
}
}
return new Response(newData);
}

The advantage of this approach is co-located handling of all the errors.

Error classes

JavaScript comes with just 7 built-in error classes:

  • EvalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError
  • InternalError

These error classes are more specific to the JavaScript programming language. At times there is a need to create new error classes to cover the common error conditions like resource not found, not accessible, unable to connect, file not found, no data, etc.

In addition to JavaScript’s 7 built-in error classes, Deno’s core runtime comes with 19 error classes that could be useful to raise error for a lot of scenarios. In this article, we’ll take a look at the uses of 19 error classes that comes bundled with Deno.

Built-in errors

Deno’s core runtime comes with 19 error classes that could eliminate the need to define custom error classes to some extent. These 19 error classes are extensively used by the JS part of Deno’s core runtime & Deno’s standard library.

Accessing error classes

Deno’s error classes are present in the global Deno namespace:

Deno.errors.<error-class>

Some examples are:

Deno.errors.NotSupported;
Deno.errors.WriteZero;

List of errors

Here is the list of 19 error classes that comes out of the box:

  • NotFound
  • PermissionDenied
  • ConnectionRefused
  • ConnectionReset
  • ConnectionAborted
  • NotConnected
  • AddrInUse
  • AddrNotAvailable
  • BrokenPipe
  • AlreadyExists
  • InvalidData
  • TimedOut
  • Interrupted
  • WriteZero
  • UnexpectedEof
  • BadResource
  • Http
  • Busy
  • NotSupported

The above errors come bundled with the core runtime, i.e. without any explicit imports. Some of the above errors are more specific to internal working of HTTP servers like AddrNotAvailable, AddrInUse, etc., while some are specific to Deno’s sandboxing like PermissionDenied.

We’ll have a look at more generic ones that could be used to handle some common errors in any application.

NotFound

The requested resource/data is not found. Some use cases are— file not found, record not found, etc.

//some module
async function getUser(userId:string) {
const rec=await getUserFromDb(userId);
if(!rec)
throw new Deno.errors.NotFound();
return rec;
}
//main module
async function reqHandler(req: Request) {
let user;
try {
user = await getUser(req.body.email);
} catch (err) {
if (err instanceof NotFound) {
return new Response(null, { status: 404 });
} else {
return new Response(null, { status: 500 });
}
}
return new Response(user);
}

AlreadyExists

The data to be created already exists in the system. Some use cases are — a record with user email already exists, a file with the same name already exists, etc.

//some module
async function createUser(userData:any) {
try {
return await createUserInDb(userData);
} catch(e) {
if(e instanceof WriteError)
throw new Deno.errors.AlreadyExists();
throw new Deno.errors.BadResource();
}
return rec;
}
//main module
async function reqHandler(req: Request) {
let user;
try {
user = createUser(req.body);
} catch (err) {
if (err instanceof AlreadyExists) {
return new Response(null, { status: 422 });
} else {
return new Response(null, { status: 500 });
}
}
return new Response(null, { status: 204 });
}

InvalidData

The data provided as input doesn’t contain expected values. This is useful for validation functions.

//some module
function checkEmail(email:string) {
if(/^[a-zA-Z0-9_]+$/.test(email))
throw new Deno.errors.InvalidData();
return true;
}
//main module
async function reqHandler(req: Request) {
let user;
try {
checkEmail(req.body.email);
const user = await getUser(req.body.email);
} catch (err) {
if (err instanceof AlreadyExists) {
return new Response(null, { status: 422 });
} else if(err instanceof InvalidData) {
return new Response(null, { status: 400 });
} else {
return new Response(null, { status: 500 });
}
}
return new Response(null, { status: 204 });
}

BadResource

The requested resource/data is not in an acceptable state. For example — processing some formatted data that’s not in valid form, etc.

//some module
async function processCSV(csvPath:string) {
const csv=await Deno.readFile(csvPath);
const tokens=csv.split(',');
if(!tokens[0] || tokens[0].startsWith('Header'))
throw new Deno.errors.BadResource();
//keep processing
}
//main module
async function reqHandler(req: Request) {
try {
await processCSV(req.body.csvPath);
} catch (err) {
if (err instanceof BadResource) {
return new Response(null, { status: 422 });
} else {
return new Response(null, { status: 500 });
}
}
return new Response(null, { status: 204 });
}

NotSupported

The requested functionality isn’t supported (yet).

//some module
function getFeature(featureName:string) {
switch(featureName) {
case 'v1':
return new someFeatureImpl();
case 'v2':
return new someOtherFeatureImpl();
...
default:
throw new Deno.errors.NotSupported();
}
}
//main module
async function reqHandler(req: Request) {
try {
const f=getFeature(new URL(req.url).pathname.split('/')[0]);
//do something with feature
} catch (err) {
if (err instanceof NotSupported) {
return new Response(null, { status: 501 });
} else {
return new Response(null, { status: 500 });
}
}
return new Response(null, { status: 204 });
}

Custom errors

We’ve seen the commonly useful errors provided by Deno’s core runtime. They are generic, but may not likely be enough to cover application specific error conditions. The applications can define their own error classes and place them in the Deno namespace inside the errors object. This way, there is no need to import errors.

The following code creates a custom error called Inaccessible and places it in Deno namespace, thereby making it globally available to all the modules.

class Inaccessible extends Error {
constructor() {
super();
this.name = "Inaccessible";
}
}
Deno.errors.Inaccessible=Inaccessible;
Object.freeze(Deno.errors);
//... some code ...throw new Deno.errors.Inaccessible();

Let’s run the above code:

$ deno run --no-check app.ts 
error: Uncaught Inaccessible
throw new Deno.errors.Inaccessible();
^
at file:///Users/mayankc/Work/source/denoExamples/app.ts:9:7

--

--