Three things that typically don't go together.

Node is asynchronous but does not have multithread support, which means that for most things, you can write simple, concise code that doesn't block IO. Here's an example:

This does not block IO,

import fs from 'fs';

fs.readFile('/etc/hosts', 'utf8', (err, data) => {
  if (err)
    return console.err(err);
  console.log(data);
});

...while this does, griding any other parallel operations to a halt:

import fs from 'fs';
const data = fs.readFileSync('/etc/hosts', 'utf-8');
// Blocks IO!

That's not any new information.

However, I was recently experimenting with methods of safely evaluating JavaScript code, and came across safe-eval, which essentially runs your code inside of a V8 sandbox. That's all well and good for asynchronous code, but as soon as someone throws a while(1); in there, you're in a bit of trouble. (Disclaimer: do not trust random JavaScript code inputted by users EVER-- even if you think it's safe!)

So, the real problem here is that node is fundamentally single-threaded, which means that we're running everything in the same thread. This got me thinking: what if we use child_process to fork out another node process and run our script? Node supports [Function].toString() for the most part, so this shouldn't be too hard, right? Right!

First, we start out with our NodeThread class:

import { spawn } from 'child_process';

export default class NodeThread {
    constructor(fn, ob) {
        this.nodePath = process.argv[0];
        this.function = `(${fn.toString()}).bind(this)(JSON.parse(${JSON.stringify(JSON.stringify(ob || {}))}));`;
    }

    run(data) {
        this.proc = spawn(process.argv[0], ['-e', this.function]);
        this.proc.stdout.setEncoding('utf8');
        this.proc.stdout.on('data', data || (() => { }));
        this.proc.on('close', this.done || (() => { }));
        return this;
    }

    done(func) {
        this.done = func;
        return this;
    }
}

First impressions are probably: what is that crazy concatenation? Well, let's have a look at it written out.

(
    ${fn.toString()}
).bind(this)
    (
        JSON.parse(
            ${
                JSON.stringify(
                    JSON.stringify(ob || {})
                )
            }
        )
    );

We can essentially trust fn.toString() -- this is never going to be input from the user so its security should be treated the same as eval.
The double stringification is used to parse the JSON on the new thread-- we could theoretically just dump the JSON in there but it could cause some syntax issues (let's stay on the safe side).

So! A simple use case:

import NodeThread from './thread/NodeThread';

new NodeThread(() => {
    process.stdout.write('test');
}).run(data => process.stdout.write(data));

Output:

test

Well, that wasn't too hard, was it?
What if we need to pass some objects in there?

Not a problem:

import NodeThread from './thread/NodeThread';

new NodeThread((ob) => {
    process.stdout.write(ob.test);
}, {test: "test!"}).run(data => process.stdout.write(data));

Okay, so all the good stuff aside: you can't pass functions directly in because of the JSON stringification, so your best bet would to be to require another helper file inside to run in the new thread.

Right, so let's install safe-eval and give this a shot

import NodeThread from './thread/NodeThread';

new NodeThread(() => {
    const safeEval = require('safe-eval');
    process.stdout.write(JSON.stringify(safeEval('1+1')))
}).run(data => process.stdout.write(data));

Output:

2

Awesome! So, now let's write a little abstraction.

ThreadedSafeEval class:

import NodeThread from './NodeThread';

export default function (evalString, context = {}, done) {
    return new NodeThread((ob) => {
        const safeEval = require('safe-eval');
        process.stdout.write(JSON.stringify(safeEval(ob.evalString, ob.context)));
    }, {evalString, context}).run(data => done(JSON.parse(data)));
}

and a quick test:

import ThreadedSafeEval from './thread/ThreadedSafeEval';

ThreadedSafeEval('1+1', {}, (resp) => console.log(resp));
2

Cool, huh?

Note: DO NOT USE THIS IN PRODUCTION-- it's just a little experiment! :)