Promise.try in JavaScript: Clean Error Handling for Sync and Async Functions
Learn how Promise.try() helps normalize synchronous and asynchronous callback errors in JavaScript, with practical patterns for APIs, validators, job runners, and Node.js services.
JS Interview Prep Editorial Team
Author
6/21/2026
Published
2 views
Views
Promise.try in JavaScript: Clean Error Handling for Sync and Async Functions
A large amount of production JavaScript accepts functions from the outside. A validation library accepts a validator. A job runner accepts a task. A plugin system accepts hooks. A route wrapper accepts a handler. The problem is that these functions may return a value, throw synchronously, return a promise, or reject asynchronously.
Promise.try() gives developers a standard way to turn all of those outcomes into one promise chain. That makes error handling more uniform and removes a small but common trap: Promise.resolve(fn()) does not catch errors thrown before Promise.resolve() receives the value.
Why this is a useful long-tail topic
Many developers search for the difference between Promise.resolve().then(fn), Promise.resolve(fn()), async wrappers, and Bluebird Promise.try. The native Promise.try() method is newer, specific, and practical. That combination makes it a good article topic for search traffic from developers solving real bugs.
- How do I catch sync and async errors in one Promise chain?
- What is Promise.try() used for?
- Promise.try versus Promise.resolve().then()
- How to wrap plugin callbacks safely in JavaScript
The bug Promise.try() prevents
This pattern looks reasonable, but it does not catch a synchronous throw from action() because action() runs before Promise.resolve() is called.
function runAction(action) {
return Promise.resolve(action())
.then((value) => ({ ok: true, value }))
.catch((error) => ({ ok: false, error }));
}
runAction(() => {
throw new Error('Validation failed');
});
// The throw escapes before Promise.resolve() can wrap it.Developers often discover this only after a route, cron job, or plugin handler crashes in a path they assumed was promise-safe.
The Promise.try() version
Promise.try() calls the function and converts all outcomes into promise behavior. A returned value becomes a fulfilled promise. A thrown error becomes a rejected promise. A returned promise is followed normally.
function runAction(action) {
return Promise.try(action)
.then((value) => ({ ok: true, value }))
.catch((error) => ({ ok: false, error }));
}
await runAction(() => 'sync value');
await runAction(async () => 'async value');
await runAction(() => {
throw new Error('sync failure');
});
await runAction(async () => {
throw new Error('async failure');
});The calling code no longer needs to know whether the action is sync or async. That is the main value.
Where Promise.try() fits in backend code
Promise.try() is especially useful when you are building an abstraction that accepts developer-provided callbacks. In application code where you control everything, async/await with try/catch is often clearer. In framework-like code, Promise.try() can make a wrapper safer and shorter.
function createRoute(handler) {
return (req, res, next) => {
Promise.try(handler, req, res)
.catch(next);
};
}
const route = createRoute((req, res) => {
if (!req.user) throw new Error('Unauthorized');
return res.json({ ok: true });
});- Route wrappers that accept sync or async handlers.
- Plugin hooks that may return plain values or promises.
- Validation pipelines where validators can throw or reject.
- Background job runners where tasks come from many modules.
Promise.try() versus async/await
Async/await is still the default choice for most application code because it is readable and familiar. Promise.try() becomes more useful when you want a reusable wrapper that stays promise-native.
async function runWithAsyncAwait(action) {
try {
const value = await action();
return { ok: true, value };
} catch (error) {
return { ok: false, error };
}
}
function runWithPromiseTry(action) {
return Promise.try(action)
.then((value) => ({ ok: true, value }))
.catch((error) => ({ ok: false, error }));
}Both versions are valid. The Promise.try() version communicates that the goal is to lift an unknown callback into promise form.
Performance and timing details
Promise.try() calls the callback synchronously. That makes it different from Promise.resolve().then(fn), where fn runs in a later microtask. This timing usually does not matter for business logic, but it can matter in tests, instrumentation, or code that intentionally relies on when side effects happen.
console.log('before');
Promise.try(() => {
console.log('inside Promise.try');
});
Promise.resolve().then(() => {
console.log('inside then');
});
console.log('after');
// before
// inside Promise.try
// after
// inside thenInterview explanation
A good interview answer: Promise.try() is for normalizing a callback that might return a value, throw, return a promise, or reject. It prevents the bug where Promise.resolve(fn()) fails to catch a synchronous throw because fn() executes before Promise.resolve() receives anything.
Then add the nuance: async/await with try/catch is often clearer in application code, but Promise.try() is useful in utilities, wrappers, libraries, and callback orchestration.
Quick FAQ
Is Promise.try() the same as Promise.resolve().then(fn)?
No. Promise.resolve().then(fn) calls fn asynchronously in a microtask. Promise.try() calls fn synchronously and wraps the result or thrown error into a promise.
Should I replace all try/catch blocks with Promise.try()?
No. Use async/await and try/catch for normal application flow. Use Promise.try() when you are wrapping unknown sync-or-async callbacks.
Why is this useful in interviews?
It shows you understand promise timing, synchronous throws, rejected promises, and the difference between application code and reusable abstractions.
