21

I have the following function:

/**
 * Retrieves a component template from filesystem
 */
const getComponentTemplate = async (
  p: string
): Promise<string> => {
  let template: string
  try {
    template = await fs.readFile(p, {
      encoding: 'utf8'
    })
  } catch (e) {
    if (e instanceof Error && e.code === 'ENOENT') {
      throw new Error(`template for element type ${elementType} not found`)
    }
    throw e
  }

  return template
}

Typescript complains here:

[ts] Property 'code' does not exist on type 'Error'

This is because the Javascript Error class only has properties message and name.

However, Node's Error class does have a code property.

Typescript defines this in a special interface ErrnoException (see source here). I have added @types/node to my package.json, but this didn't make Typescript realize that this Error is part of the ErrnoException interface.

It is not possible to declare a type annotation in a catch clause. So, how does one make the Typescript compiler able to resolve that this is a Node Error?

FYI, this is part of my tsconfig.json:

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "lib": ["es2017"]
    ...
  }
}
6
  • 3
    e instanceof Error you're testing to see if it's an Error not an ErrnoException. I'm not sure on the best way to tell it the right type, but a "quick n dirty" way is e instanceof Error && (e as ErrnoException).code === 'ENOENT'. Commented Jul 25, 2018 at 16:31
  • 3
    You can probably make a typeguard along the lines of this: function isError(error: any): error is ErrnoException { return error instanceof Error; } though it'd surprise me if something like that doesn't already exist. You'd this this would be a common requirement! Commented Jul 25, 2018 at 16:36
  • I realize that I could use as but this seems somewhat dirty. Shouldn't Typescript know that I am running in Node? Commented Jul 25, 2018 at 17:07
  • Wouldn't if (e instanceof ErrnoException) just fit your needs? Commented Jul 25, 2018 at 17:21
  • @arvymetal no because the class is actually Error (the one provided by Node). ErrnoException is only an interface, not a class. Therefore e is not an instance of it. Note that Node's Error implements ErrnoException, but for some reason Typescript doesn't realize this. Commented Jul 25, 2018 at 17:23

6 Answers 6

12

Type-safe TypeScript solution

It is not the universal solution, but works for the ErrnoException case. According "@types/node": "16.11.xx" definitins, the ErrnoException is the interface:

interface ErrnoException extends Error {
   errno?: number | undefined;
   code?: string | undefined;
   path?: string | undefined;
   syscall?: string | undefined;
}

Below type guard fully respects this defenition. My TypeScript and ESLint settings are pretty strict, so with a high probability you will not need the comments disabling the ESLint/TSLint (if you still use this depricated one).

function isErrnoException(error: unknown): error is ErrnoException {
  return isArbitraryObject(error) &&
    error instanceof Error &&
    (typeof error.errno === "number" || typeof error.errno === "undefined") &&
    (typeof error.code === "string" || typeof error.code === "undefined") &&
    (typeof error.path === "string" || typeof error.path === "undefined") &&
    (typeof error.syscall === "string" || typeof error.syscall === "undefined");
}

where

type ArbitraryObject = { [key: string]: unknown; };

function isArbitraryObject(potentialObject: unknown): potentialObject is ArbitraryObject {
  return typeof potentialObject === "object" && potentialObject !== null;
}

Now we can check the code property:

import FileSystem from "fs";
import PromisfiedFileSystem from "fs/promises";

// ...

let targetFileStatistics: FileSystem.Stats;

try {

  targetFileStatistics = await PromisfiedFileSystem.stat(validAbsolutePathToPublicFile);

} catch (error: unknown) {

  if (isErrnoException(error) && error.code === "ENOENT") {

     response.
         writeHead(HTTP_StatusCodes.notFound, "File not found.").
         end();

     return;
  }

 
  response.
      writeHead(HTTP_StatusCodes.internalServerError, "Error occurred.").
      end();
}

I added isErrnoException type guard to my library @yamato-daiwa/es-extensions-nodejs but because I know that the promotion of third-party solutions could be annoying, I have posted the full implementation with usage example above =)

Sign up to request clarification or add additional context in comments.

Comments

8

This is not too ugly and works:

e instanceof Error && 'code' in e && e.code === 'ENOENT'

Comments

5

I ended up using @AndyJ's comment:

/**
 * Retrieves a component template from filesystem
 */
const getComponentTemplate = async (
  p: string
): Promise<string> => {
  let template: string
  try {
    template = await fs.readFile(p, {
      encoding: 'utf8'
    })
  } catch (e) {
    // tslint:disable-next-line:no-unsafe-any
    if (isNodeError(e) && e.code === 'ENOENT') {
      throw new Error(`template for element type ${elementType} not found`)
    }
    throw e
  }

  return template
}

/**
 * @param error the error object.
 * @returns if given error object is a NodeJS error.
 */
const isNodeError = (error: Error): error is NodeJS.ErrnoException =>
  error instanceof Error

But I am surprised to see that this is necessary. Also it requires you to disable tslint's unsafe-any rule if you are using that.

3 Comments

Unbelievable that this is still the best solution to this problem.
I think that the guard should be: const isNodeError = (error: unknown): error is NodeJS.ErrnoException => error instanceof Error Since the purpose is to refine from unknown to a known type.
I slightly modified it too -> const isNodeError = (error: Error | unknown): error is NodeJS.ErrnoException => error instanceof Error as an exception doesn't have to be of type Error, and saves having to do another instance check.
5

If you want to use try/catch then you'll be getting an object you don't know the type of.

The code you already have tests to see if that object is an Error, and if it is then it casts it as a "normal" JS Error object.

You can use a typeguard to tell the type system what type the object actually is.

Something along the lines of:

function isError(error: any): error is ErrnoException { return error instanceof Error; }

I had a look at fs.readFile and it seems a common way of using that function, and indeed the entire node api, is by passing it a callback which gets called either when the job is done, or there has been an error.

And looking at the type definition it shows that the error object passed to the callback is indeed the desired ErrnoException.

export function readFile(path: PathLike | number, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void;

So using the callback will eliminate the need for the type guard, and seems to be the node way of approaching this.

This article apparently details some of the thinking behind the "callback all the things" approach.

Node’s heavy use of callbacks dates back to a style of programming older than JavaScript itself. Continuation-Passing Style (CPS) is the old-school name for how Node.js uses callbacks today. In CPS, a “continuation function” (read: “callback”) is passed as an argument to be called once the rest of that code has been run. This allows different functions to asynchronously hand control back and forth across an application.

Node.js relies on asynchronous code to stay fast, so having a dependable callback pattern is crucial. Without one, developers would be stuck maintaining different signatures and styles between each and every module. The error-first pattern was introduced into Node core to solve this very problem, and has since spread to become today’s standard. While every use-case has different requirements and responses, the error-first pattern can accommodate them all.

2 Comments

Thanks for the input! The typeguard seems to solve the problem, though I have to use a tslint ignore comment for the use of any. That pro-callback post is from 2014. Since then, much has changed. People tend to avoid callbacks to avoid deeply nested structures. Async / await is heavily adopted and I would not consider it a solution to go back to the old days because Typescript fails to resolve that an Error class is actually from node and not es6.
I would like to accept your answer for the Typeguard, but not for the callback suggestion. ;)
2

You may consider reading the code property using square brackets and then checking if its value equals ENOENT:

try {
    ...
} catch (e) {
    const code: string = e['code'];
    if (code === 'ENOENT') {
        ...
    }
    throw e
}

This isn't a perfect solution, but it may be good enough considering that you cannot declare types in catch clauses and that the e instanceof ErrnoException check doesn't work properly (as discussed in the question comments).

1 Comment

Thank you -- it would be surprising to me if this is the recommended approach, considering how common error handling is.
2

I think the snippets below are more correct. Notice the || instead of &&.

const isNodeError = (error: any): error is NodeJS.ErrnoException => {
  if (error instanceof Error) {
    return 'errno' in error || 'code' in error || 'path' in error || 'syscall' in error;
  }
  return false;
};
const isNodeError = (error: any): error is NodeJS.ErrnoException => {
  if (error instanceof Error) {
    const nodeError: NodeJS.ErrnoException = error;
    return (
      (typeof nodeError.errno === 'number') ||
      (typeof nodeError.code === 'string') ||
      (typeof nodeError.path === 'string') ||
      (typeof nodeError.syscall === 'string')
    );
  }
  return false;
};

If you are using only the code property, you can simplify it even more:

const isNodeError = (error: any): error is NodeJS.ErrnoException => {
  return error instanceof Error && 'code' in error;
};
const isNodeError = (error: any): error is NodeJS.ErrnoException => {
  return error instanceof Error && typeof (<NodeJS.ErrnoException>error).code === 'string';
};

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.