Lập trình · 23/10/2026

Promise.then: Why the Second Callback Misses Errors in the First

promise.then(onSuccess, onError) does not have the same error boundary as promise.then(onSuccess).catch(onError). Confusing them can leave parsing failures outside the handler you expected to run. Track which Promise each step observes.

Promise.then: vì sao callback lỗi thứ hai không bắt được lỗi trong callback thành công?

promise.then(onSuccess, onError) does not have the same error boundary as promise.then(onSuccess).catch(onError). Confusing them can leave parsing failures outside the handler you expected to run. Track which Promise each step observes.

1. The second callback handles the input Promise's rejection

const events = [];
await Promise.resolve('{bad json}')
  .then(
    text => JSON.parse(text),
    () => { events.push('source failed'); }
  )
  .catch(error => {
    events.push(error instanceof SyntaxError ? 'parse failed' : 'other');
  });
console.log(events.join(', ')); // parse failed

The input Promise fulfills, so parsing runs. The thrown exception rejects the Promise returned by then; the second callback in that same then call does not handle this new rejection. The following catch observes it.

As the MDN Promise.then reference explains, then returns another Promise whose outcome depends on the callback. Review each link rather than treating the entire expression as a single try/catch block.

2. A catch after transformation covers both failure sources

async function parsePayload(source) {
  return source
    .then(text => JSON.parse(text))
    .catch(error => {
      throw new Error('Cannot parse payload', { cause: error });
    });
}
for (const source of [
  Promise.resolve('{"count":0}'),
  Promise.resolve('{bad json}')
]) {
  try {
    console.log((await parsePayload(source)).count); // 0
  } catch (error) {
    console.log(error.cause instanceof SyntaxError); // true
  }
}

The helper expects a standard Promise fulfilled with JSON text. Its catch receives source rejection as well as a parsing exception. It adds context and throws, so callers still need to await and handle failure. The cause preserves the original reason.

Successful JSON parsing does not validate a schema. The example uses an object with count for illustration; production code must check types, required fields and ranges separately. Avoid classifying every failure as a network error when transformation can also fail.

3. Handling an error can silently restore success

const events = [];
const result = await Promise.reject(new Error('offline'))
  .catch(() => { events.push('reported'); });
console.log(result === undefined); // true
console.log(events.join(', ')); // reported

const fallback = await Promise.reject(new Error('offline'))
  .catch(() => ({ count: 0 }));
console.log(fallback.count); // 0

A catch callback that finishes without returning a value fulfills the next Promise with undefined. Returning a fallback object also restores success, with explicit data. The MDN Promise.catch reference describes catch as shorthand for then(undefined, onRejected).

Use fallback data only when the product permits it. A fabricated zero count can make “failed to load” look like “no records”; the interface needs an appropriate failure or stale-data state. To report and propagate failure, throw again or return a rejected Promise.

4. Chain membership defines the error boundary

A catch observes work connected to its chain. If a callback starts another asynchronous task, return or await that task when its rejection belongs to the same flow. Calling it and discarding its Promise does not extend the catch boundary.

Using both then callbacks can deliberately separate source failures from transformation failures. Keep a handler for the returned Promise's rejection as well. Choose the structure to express failure categories rather than mechanically replacing every then call.

5. Review before changing a Promise chain

  • Which Promise does each rejection handler observe?
  • Do tests cover source rejection and exceptions inside fulfillment callbacks?
  • Are child task Promises returned or awaited?
  • Does catch restore success with data or accidentally return undefined?
  • Can callers still access the original reason and required failure state?

All six bilingual blocks ran with Node.js. Additional checks verify source rejection causes, invalid JSON and successful parsing. Real HTTP services and browser interfaces were not integrated.

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.