Node.js
error handling
synchronous functions
JavaScript
programming tips

How to catch errors in synchronous functions in node.js?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

For synchronous Node.js code, error handling is straightforward: if a function throws, catch it with try and catch in the same call stack. The important limit is that try/catch only handles errors thrown synchronously. Once the code crosses into callbacks or promises, the error-handling model changes, which is why many “why did this not get caught” bugs are really timing-model misunderstandings.

Catch Thrown Errors with try/catch

If a synchronous function throws an error, wrap the call site in try/catch:

javascript
1function parsePort(value) {
2  const port = Number(value);
3
4  if (!Number.isInteger(port) || port <= 0) {
5    throw new Error("Invalid port");
6  }
7
8  return port;
9}
10
11try {
12  const port = parsePort("abc");
13  console.log(port);
14} catch (error) {
15  console.error("Failed to parse port:", error.message);
16}

This works because parsePort() throws before control leaves the current synchronous execution path. The exception bubbles up until a matching catch block handles it.

The same pattern applies to built-in synchronous APIs such as JSON.parse() or fs.readFileSync(). If the operation throws immediately, try/catch is the correct tool.

Keep the try Block Focused

A small try block is usually better than wrapping half the application:

javascript
1try {
2  const config = JSON.parse(input);
3  validateConfig(config);
4} catch (error) {
5  console.error("Invalid configuration:", error.message);
6}

This keeps the failure surface clear. If the block is too large, it becomes harder to tell which operation actually caused the exception.

You can also rethrow after adding context:

javascript
1function loadConfig(text) {
2  try {
3    return JSON.parse(text);
4  } catch (error) {
5    throw new Error(`Config parse failed: ${error.message}`);
6  }
7}

That is useful when a low-level error needs a more domain-specific message.

try/catch Does Not Catch Async Failures

This is where many bugs come from. A try/catch around a synchronous function call will not catch errors that occur later inside asynchronous code:

javascript
1try {
2  setTimeout(() => {
3    throw new Error("This will not be caught here");
4  }, 0);
5} catch (error) {
6  console.error("Caught:", error.message);
7}

The catch block never runs because the error happens in a later event-loop turn. For callbacks, promises, and async functions, use the error-handling mechanism that belongs to that model.

For example, with promises:

javascript
1Promise.resolve()
2  .then(() => {
3    throw new Error("Promise failure");
4  })
5  .catch((error) => {
6    console.error("Caught promise error:", error.message);
7  });

Use finally for Cleanup

If synchronous code opens resources or acquires temporary state, finally ensures cleanup runs whether the operation succeeds or fails:

javascript
1let locked = false;
2
3try {
4  locked = true;
5  throw new Error("Something failed");
6} catch (error) {
7  console.error(error.message);
8} finally {
9  locked = false;
10}

finally is not a replacement for error handling, but it is the right place for guaranteed cleanup.

Common Pitfalls

The biggest mistake is expecting try/catch to handle asynchronous errors automatically. It does not. It only catches errors thrown before the current synchronous stack unwinds.

Another issue is writing very large try blocks. That makes debugging harder because unrelated failures are all handled in one place with no clear boundary. Small, focused try blocks make logs and stack traces much easier to interpret later.

Some code also catches an error and silently ignores it. Unless the error is truly expected and harmless, either log it, handle it meaningfully, or rethrow it.

Finally, avoid using exceptions for ordinary control flow. Throw when the state is genuinely exceptional, not as a substitute for normal branching logic.

Summary

  • Use try/catch to handle errors thrown by synchronous Node.js functions.
  • Catch at the call site or add context before rethrowing.
  • Keep try blocks narrow so failures stay easy to diagnose.
  • Use finally for cleanup that must always run.
  • Do not expect try/catch to catch errors that happen later in async code.

Course illustration
Course illustration

All Rights Reserved.