diff --git a/src/__tests__/__snapshots__/error_handling.test.ts.snap b/src/__tests__/__snapshots__/error_handling.test.ts.snap index e5c10a3..81580da 100644 --- a/src/__tests__/__snapshots__/error_handling.test.ts.snap +++ b/src/__tests__/__snapshots__/error_handling.test.ts.snap @@ -262,7 +262,7 @@ exports[`sandbox custom ErrorHandler properly handles InvalidCommandError 1`] = - Error: SyntaxError: Unexpected identifier 'foo' + SyntaxError: Unexpected identifier 'foo' @@ -284,7 +284,7 @@ exports[`sandbox custom ErrorHandler properly handles InvalidCommandError 1`] = exports[`sandbox custom ErrorHandler properly handles InvalidCommandError 2`] = ` [ - [Error: SyntaxError: Unexpected identifier 'foo'], + [SyntaxError: Unexpected identifier 'foo'], ] `; diff --git a/src/__tests__/__snapshots__/images.test.ts.snap b/src/__tests__/__snapshots__/images.test.ts.snap index 5e50b5f..a591eac 100644 --- a/src/__tests__/__snapshots__/images.test.ts.snap +++ b/src/__tests__/__snapshots__/images.test.ts.snap @@ -1187,7 +1187,7 @@ exports[`001: Issue #61 Correctly renders an SVG image 1`] = ` } `; -exports[`002: throws when thumbnail is incorrectly provided when inserting an SVG 1`] = `[Error: Error executing command 'IMAGE svgImgFile()': An extension (one of .png,.gif,.jpg,.jpeg,.svg) needs to be provided when providing an image or a thumbnail.]`; +exports[`002: throws when thumbnail is incorrectly provided when inserting an SVG 1`] = `[Error: Error executing command 'IMAGE svgImgFile()': Error: An extension (one of .png,.gif,.jpg,.jpeg,.svg) needs to be provided when providing an image or a thumbnail.]`; exports[`003: can inject an svg without a thumbnail 1`] = ` { diff --git a/src/__tests__/__snapshots__/templating.test.ts.snap b/src/__tests__/__snapshots__/templating.test.ts.snap index b23571f..1982f31 100644 --- a/src/__tests__/__snapshots__/templating.test.ts.snap +++ b/src/__tests__/__snapshots__/templating.test.ts.snap @@ -19868,7 +19868,7 @@ exports[`noSandbox Template processing 39 Processes LINK commands 1`] = ` } `; -exports[`noSandbox Template processing 40 Throws on invalid command 1`] = `[Error: Error executing command 'TTT foo': Unexpected identifier 'foo']`; +exports[`noSandbox Template processing 40 Throws on invalid command 1`] = `[Error: Error executing command 'TTT foo': SyntaxError: Unexpected identifier 'foo']`; exports[`noSandbox Template processing 41 Throws on invalid for logic 1`] = `[Error: Invalid command: END-FOR company]`; @@ -26172,14 +26172,14 @@ exports[`noSandbox Template processing 107b non-alphanumeric INS commands (e.g. exports[`noSandbox Template processing 112a failFast: false lists all errors in the document before failing. 1`] = ` [ - [Error: Error executing command 'notavailable': notavailable is not defined], - [Error: Error executing command 'something': something is not defined], + [Error: Error executing command 'notavailable': ReferenceError: notavailable is not defined], + [Error: Error executing command 'something': ReferenceError: something is not defined], [Error: Invalid command: END-FOR company], [Error: Unterminated FOR-loop ('FOR company'). Make sure each FOR loop has a corresponding END-FOR command.], ] `; -exports[`noSandbox Template processing 112b failFast: true has the same behaviour as when failFast is undefined 1`] = `[Error: Error executing command 'notavailable': notavailable is not defined]`; +exports[`noSandbox Template processing 112b failFast: true has the same behaviour as when failFast is undefined 1`] = `[Error: Error executing command 'notavailable': ReferenceError: notavailable is not defined]`; exports[`noSandbox Template processing 131 correctly handles Office 365 .docx files 1`] = ` { @@ -30290,7 +30290,7 @@ exports[`noSandbox Template processing avoids confusion between variable name an } `; -exports[`noSandbox Template processing fixSmartQuotes flag (see PR #152) 1`] = `"Error executing command 'reverse(‘aubergine’)': Invalid or unexpected token"`; +exports[`noSandbox Template processing fixSmartQuotes flag (see PR #152) 1`] = `"Error executing command 'reverse(‘aubergine’)': SyntaxError: Invalid or unexpected token"`; exports[`noSandbox Template processing iterate over object properties and keys in FOR loop 1`] = ` { diff --git a/src/__tests__/error_handling.test.ts b/src/__tests__/error_handling.test.ts index 01538bf..3e79729 100644 --- a/src/__tests__/error_handling.test.ts +++ b/src/__tests__/error_handling.test.ts @@ -4,6 +4,7 @@ import QR from 'qrcode'; import { createReport } from '../index'; import { setDebugLogSink } from '../debug'; import { + isError, NullishCommandResultError, CommandExecutionError, InvalidCommandError, @@ -11,6 +12,17 @@ import { if (process.env.DEBUG) setDebugLogSink(console.log); +class NoErrorThrownError extends Error {} + +const getError = async (call: () => unknown): Promise => { + try { + await call(); + throw new NoErrorThrownError(); + } catch (error: unknown) { + return error as TError; + } +}; + ['noSandbox', 'sandbox'].forEach(sbStatus => { const noSandbox = sbStatus === 'sandbox' ? false : true; @@ -325,3 +337,64 @@ if (process.env.DEBUG) setDebugLogSink(console.log); ); }); }); + +describe('errors from different realms', () => { + it('sandbox', async () => { + const template = await fs.promises.readFile( + path.join(__dirname, 'fixtures', 'referenceError.docx') + ); + + const error = await getError(() => + createReport({ noSandbox: false, template, data: {} }) + ); + expect(error).toBeInstanceOf(CommandExecutionError); + + // We cannot check with instanceof as this Error is from another realm despite still being an error + const commandExecutionError = error as CommandExecutionError; + expect(commandExecutionError.err).not.toBeInstanceOf(ReferenceError); + expect(commandExecutionError.err).not.toBeInstanceOf(Error); + expect(commandExecutionError.err.name).toBe('ReferenceError'); + expect(commandExecutionError.err.message).toBe( + 'nonExistentVariable is not defined' + ); + }); + + it('noSandbox', async () => { + const template = await fs.promises.readFile( + path.join(__dirname, 'fixtures', 'referenceError.docx') + ); + + const error = await getError(() => + createReport({ noSandbox: true, template, data: {} }) + ); + expect(error).toBeInstanceOf(CommandExecutionError); + + // Without sandboxing, the error is from the same realm + const commandExecutionError = error as CommandExecutionError; + expect(commandExecutionError.err).toBeInstanceOf(ReferenceError); + expect(commandExecutionError.err).toBeInstanceOf(Error); + expect(commandExecutionError.err.name).toBe('ReferenceError'); + expect(commandExecutionError.err.message).toBe( + 'nonExistentVariable is not defined' + ); + }); +}); + +describe('isError', () => { + it('Error is an error', () => { + expect(isError(new Error())).toBeTruthy(); + }); + + it('error-like object is an error', () => { + expect( + isError({ + name: 'ReferenceError', + message: 'nonExistentVariable is not defined', + }) + ).toBeTruthy(); + }); + + it('primitive is not an error', () => { + expect(isError(1)).toBeFalsy(); + }); +}); diff --git a/src/__tests__/fixtures/referenceError.docx b/src/__tests__/fixtures/referenceError.docx new file mode 100644 index 0000000..d44a549 Binary files /dev/null and b/src/__tests__/fixtures/referenceError.docx differ diff --git a/src/errors.ts b/src/errors.ts index 036a4f2..24fc7a0 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,5 +1,12 @@ import { LoopStatus } from './types'; +export function isError(err: unknown): err is Error { + return ( + err instanceof Error || + (typeof err === 'object' && !!err && 'name' in err && 'message' in err) + ); +} + /** * Thrown when `rejectNullish` is set to `true` and a command returns `null` or `undefined`. */ @@ -50,7 +57,7 @@ export class CommandExecutionError extends Error { command: string; err: Error; constructor(err: Error, command: string) { - super(`Error executing command '${command}': ${err.message}`); + super(`Error executing command '${command}': ${err.name}: ${err.message}`); Object.setPrototypeOf(this, CommandExecutionError.prototype); this.command = command; this.err = err; diff --git a/src/jsSandbox.ts b/src/jsSandbox.ts index 6f8329e..34ae365 100755 --- a/src/jsSandbox.ts +++ b/src/jsSandbox.ts @@ -1,7 +1,11 @@ import vm from 'vm'; import { getCurLoop } from './reportUtils'; import { ReportData, Context, SandBox } from './types'; -import { CommandExecutionError, NullishCommandResultError } from './errors'; +import { + isError, + CommandExecutionError, + NullishCommandResultError, +} from './errors'; import { logger } from './debug'; // Runs a user snippet in a sandbox, and returns the result. @@ -51,7 +55,7 @@ export async function runUserJsAndGetRaw( result = await script.runInContext(context); } } catch (err) { - const e = err instanceof Error ? err : new Error(`${err}`); + const e = isError(err) ? err : new Error(`${err}`); if (ctx.options.errorHandler != null) { context = sandbox; result = await ctx.options.errorHandler(e, code); diff --git a/src/processTemplate.ts b/src/processTemplate.ts index 91cfe5b..b96c458 100755 --- a/src/processTemplate.ts +++ b/src/processTemplate.ts @@ -25,6 +25,7 @@ import { NonTextNode, } from './types'; import { + isError, CommandSyntaxError, InternalError, InvalidCommandError, @@ -628,7 +629,7 @@ const processCmd: CommandProcessor = async ( try { processImage(ctx, img); } catch (e) { - if (!(e instanceof Error)) throw e; + if (!isError(e)) throw e; throw new ImageError(e, cmd); } } @@ -660,7 +661,7 @@ const processCmd: CommandProcessor = async ( } else throw new CommandSyntaxError(cmd); return; } catch (err) { - if (!(err instanceof Error)) throw err; + if (!isError(err)) throw err; if (ctx.options.errorHandler != null) { return ctx.options.errorHandler(err); }