5

Depending on the nodejs framework, there are usually two ways to manage errors.

  • Throw them (ie throw new Error('invalid id');)
  • Return them (ie return { 400: 'invalid id' };)

Due to old advice that throwing errors is inefficient, I always tried to return errors but I prefer throwing errors since they are more convenient. The only article referring to the performance hit was referring to Node v0.

Is it still true?


Update 1

Nvm, I realized I could test it myself. Following code is for reference:

import { performance } from "perf_hooks";

function ThrowException() {
    throw new Error("invalid exception");
}
const _ATTEMPT = 1000000;
function ThrowingExceptions() {
    const p1 = performance.now();
    for (let i = 0; i < _ATTEMPT; i++) {
        try {
            ThrowException();
        } catch (ex: any) {
            // log error
        }
    }
    const p2 = performance.now();
    console.log(`ThrowingExceptions: ${p2 - p1}`);
}
function ReturnException() {
    return { error: { _: "invalid exception" } };
}
function ReturningExceptions() {
    const p1 = performance.now();
    for (let i = 0; i < _ATTEMPT; i++) {
        const ex = ReturnException();
        if (ex.error) {
            // log error
        }
    }
    const p2 = performance.now();
    console.log(`ReturningExceptions: ${p2 - p1}`);
}

function Process() {
    ThrowingExceptions();
    ReturningExceptions();
    ThrowingExceptions();
    ReturningExceptions();
}

Process();

Results

ThrowingExceptions: 15961.33209991455
ReturningExceptions: 5.09220027923584
ThrowingExceptions: 16461.43380022049
ReturningExceptions: 3.0963997840881348

Update 2

Creating errors created the largest penalty. Thanks @Bergi

import { performance } from "perf_hooks";

const error = new Error("invalid exception");
function ThrowException() {
    throw error;
}
const _ATTEMPT = 1000000;
function ThrowingExceptions() {
    const p1 = performance.now();
    for (let i = 0; i < _ATTEMPT; i++) {
        try {
            ThrowException();
        } catch (ex: any) {
            // log error
        }
    }
    const p2 = performance.now();
    console.log(`ThrowingExceptions: ${p2 - p1}`);
}
function ReturnException() {
    return { error };
}
function ReturningExceptions() {
    const p1 = performance.now();
    for (let i = 0; i < _ATTEMPT; i++) {
        const ex = ReturnException();
        if (ex.error) {
            // log error
        }
    }
    const p2 = performance.now();
    console.log(`ReturningExceptions: ${p2 - p1}`);
}

function Process() {
    ThrowingExceptions();
    ReturningExceptions();
    ThrowingExceptions();
    ReturningExceptions();
}

Process();

Results

ThrowingExceptions: 2897.1585998535156
ReturningExceptions: 3.7821998596191406
ThrowingExceptions: 2905.3162999153137
ReturningExceptions: 4.0701003074646
SILENT
  • 3,916
  • 3
  • 38
  • 57
  • A question that comes to mind is how often when you're throwing an exception is the performance of that operation actually something you need to optimize? The way I write code, throwing an exception would not be the typical path through the code, but rather the unusual path through the code and using exceptions appropriately would be less code to write and test. – jfriend00 Jan 12 '22 at 06:49
  • 1
    Do you refer to [In Javascript, is it expensive to use try-catch blocks even if an exception is never thrown?](https://stackoverflow.com/q/19727905/1048572) Notice that the old issue of [V8 not being able to optimise functions that use `try`/`catch`](https://stackoverflow.com/q/38121208/1048572) is solved for years now. – Bergi Jan 12 '22 at 06:58
  • @jfriend00 I try not to over optimize but the number of times an exception may be thrown is dependent on the client. For example, if I have a route to validate email, I could throw / return an error. An attacker could try to overwhelm my services by abusing it but it would take more effort to overwhelm a more performant solution. Even though its not as convenient, it's a small price to pay to gain performance that I can burn on important factors like security. – SILENT Jan 12 '22 at 07:34
  • @Bergi I already know NodeJS optimizes for try catches. The question is the performance hit when try catches catch errors. – SILENT Jan 12 '22 at 07:36
  • 2
    I'm pretty sure the "performance hit" you observe is from creating the stack traces in `new Error`, not from catching the exception. Try `return { error: new Error(…) }` for a better comparison maybe? Also I fear that [both looping functions have the problem of unrealistic microbenchmarks](https://mrale.ph/blog/2012/12/15/microbenchmarks-fairy-tale.html). – Bergi Jan 12 '22 at 08:01
  • @SILENT "*An attacker could try to overwhelm my services by abusing it*" - a DOS attack will hit on other limiting factors of your server, not on throughput of exception-throwing code. Missing stack traces when debugging a problem is a large price to pay. – Bergi Jan 12 '22 at 08:04
  • @Bergi You're right. Error object creation was the largest reason. However, catching errors is still slower than returns. You also maybe right about the microbenchmarks but I don't know how to confirm. I thought DOS attackers aim for services with the least authentication and largest performance hits, whatever route it may be? I prefer one consistent method of error management. Some errors (ie unexpected errors) definitely need stack traces but other errors like invalid auth or email don't. I'll continue using the two types of error management. – SILENT Jan 12 '22 at 08:30
  • 1
    "*You also maybe right about the microbenchmarks but I don't know how to confirm.*" - for one, have the `catch`/`if` block actually do something, like `globalCounter += error.message.length`. That should prevent it from being optimised away. And of course, try measuring the actual impact of that error handling code on the entire http request handler. "*DOS attackers aim for services with the least authentication and largest performance hits, whatever route it may be*" - yes, but server performance is most impacted by IO (reading files, accessing databases etc) not by exception handling. – Bergi Jan 12 '22 at 08:47

0 Answers0