mde / ejs

Embedded JavaScript templates -- http://ejs.co
Apache License 2.0
7.71k stars 846 forks source link

Implements #575 Better error handling via the new options 'error' and 'processSource' #640

Open blutorange opened 2 years ago

blutorange commented 2 years ago

Closes #575 Better error handling via the new options error and processSource

From the readme:

Options

Error handling

In an ideal world, all templates are valid and all JavaScript code they contain never throws an error. Unfortunately, this is not always the case in the real world. By default, when any JavaScript code in a template throws an error, the entire templates fails and not text is rendered. Sometimes you might want to ignore errors and still render the rest of the template.

You can use the error option to handle errors within expressions (the <%=%> and <%-%> tags). This is a callback that is invoked when an unhandled error is thrown:

const ejs = require('ejs');

ejs.render('<%= valid %> <%= i.am.invalid %>', { valid: 2 }, {
  error: function(err, escapeFn) {
    console.error(err);
    return escapeFn("ERROR");
  }
});

The code above logs the error to the console and renders the text 2 ERROR.

Note that this only applies to expression, not to other control blocks such as <%if (something.invalid) { %> ... <% } %>. To handle errors in these cases, you e.g. can use the processSource option to wrap individual top-level statements in try-catch blocks. For example, by using acorn and astring for processing JavaScript source code:

const ejs = require('ejs');
const acorn = require('acorn');
const astring = require('astring');

ejs.render('<%= valid %> <%if (something.invalid) { %> foo <% } %>',
  { valid: 2 },
  {
    // Wrap all individual top-level statement in a try-catch block
    processSource: function(source, outputVar) {
      const ast = acorn.parse(source, {
        allowAwaitOutsideFunction: true,
        allowReturnOutsideFunction: true,
        ecmaVersion: 2020,
      });
      return ast.body
        .filter(node => node.type !== "EmptyStatement")
        .map(node => {
          const statement = astring.generate(node, { indent: "", lineEnd: "" });
          switch (node.type) {
            case "ReturnStatement":
            case "TryStatement":
            case "EmptyStatement":
              return statement;
            default:
              return `try{${statement}}catch(e){console.error(e);${outputVar}+='STATEMENT_ERROR'}`;
          }
        })
        .join("\n");
  },
});

The above code logs the error to the console and renders the text 2 STATEMENT_ERROR.

shimonbrandsdorfer commented 1 year ago

When I check out this PR and run tests with mocha, I get 3 failing tests. Am I missing anything? Or this is not fully implemented yet?

blutorange commented 1 year ago

I think it was done, but this PR is from some time ago, things may have changed in the mean time. I'd have to take a look at the failing tests.

shimonbrandsdorfer commented 1 year ago

Here is attached a screenshot of 3 failing tests.

ejs-fail

blutorange commented 1 year ago

@shimonbrandsdorfer

I checked out this branch and ran the tests, there was only 1 test failure; and that was because it was checking an error message that has changed in node 16 -- with node 14 all tests are passing for me.

nvm use 14
git checkout 0f337d1f8aa5e56794a8a55d54be58750fe6e3f1
npm install
# had to fix some code style issues
node node_modules/.bin/eslint "**/*.js" --fix
npx jake test

Then I also rebased against the current main branch. Here the tests are also all passing.

nvm use 16
git checkout issue-575-error-handling
npm install
# npx jake test results in an error "ReferenceError: suite is not defined" 
node ./node_modules/.bin/mocha -u tdd

I'm running on a Linux (Ubuntu) OS, perhaps there are some issues with the tests on Windows? Have you tried running the tests from the main branch? nvm, the tests that are failing for you are the newly added tests

blutorange commented 1 year ago

One of the failing tests has e instanceof ReferenceError. It seems that is also calls the error handler for you, but with a different error type. That might have to do with your environment / the node runtime? Perhaps you could change the test client mode supports error handler in client mode (file test/ejs.js) and see what type of error it gives you?

  test('supports error handler in client mode', function () {
    assert.equal(ejs.render('<%= it.does.not.exist %>', {}, {
      client: true, error: function(e){return e.toString();}
    }), 'true&amp;');
  });

For me, that prints -ReferenceError: it is not defined. But as long as the error handler is called, this PR is working as intended and it's probably the test itself that needs fixing.

image