actualbudget/actual · error

Failed to parse mapping

Error message

Failed to parse mapping: ${String(message)}

What it means

mappingsFromString parses a serialized custom-sync mapping string back into nested Maps. Any exception thrown during parsing (malformed JSON, wrong shape, non-object values) is caught and rethrown wrapped in this error with the original message appended, so the root cause is preserved in the message text.

Solutions

  1. Read the wrapped message — the original parse error after 'Failed to parse mapping:' pinpoints the syntax or shape problem.
  2. Validate the mapping string is valid JSON with shape { [category]: { [from]: to } } before calling.
  3. Regenerate the mapping string from the source app instead of hand-editing it.
  4. Fall back to defaultMappings if the custom string cannot be parsed.

Example fix

// before
const mappings = mappingsFromString(process.env.CUSTOM_SYNC_MAPPING);
// after
let mappings;
try {
  mappings = mappingsFromString(process.env.CUSTOM_SYNC_MAPPING);
} catch (e) {
  console.warn('Invalid mapping, using defaults:', e.message);
  mappings = defaultMappings;
}
Defensive patterns

Strategy: try-catch

Validate before calling

let parsed: unknown;
try { parsed = JSON.parse(mappingString); } catch { throw new Error('Mapping is not valid JSON'); }
const shapeOk = parsed && typeof parsed === 'object' &&
  Object.values(parsed).every(v => v && typeof v === 'object' && !Array.isArray(v));
if (!shapeOk) throw new Error('Mapping must be { category: { from: to } }');

Type guard

function isMappingString(s: unknown): s is string {
  if (typeof s !== 'string') return false;
  try {
    const v = JSON.parse(s);
    return !!v && typeof v === 'object' &&
      Object.values(v).every(x => x && typeof x === 'object' && !Array.isArray(x));
  } catch { return false; }
}

Try / catch

try {
  mappings = mappingsFromString(raw);
} catch (e) {
  if (e.message.startsWith('Failed to parse mapping:')) {
    console.warn(`${e.message} — falling back to defaultMappings`);
    mappings = defaultMappings;
  } else { throw e; }
}

Prevention

When it happens

Trigger: Passing a hand-edited or corrupted mapping string (invalid JSON, top-level value that is not an object of objects), an environment variable or config file containing truncated mappings, or a mappings string produced by a different/older schema version.

Common situations: Users hand-editing custom sync mapping config; upgrades changing the mapping schema so old serialized mappings no longer match; shell escaping mangling quotes in an env-var-provided mapping string.

Understand the failure class

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/e2ef1d0854187876. Report an issue: GitHub.

Appendix: source

Thrown at packages/loot-core/src/server/util/custom-sync-mapping.ts:27

      ]),
    ),
  );

export const mappingsFromString = (str: string): Mappings => {
  try {
    const parsed = JSON.parse(str);
    if (typeof parsed !== 'object' || parsed === null) {
      throw new Error('Invalid mapping format');
    }
    return new Map(
      Object.entries(parsed).map(([key, value]) => [
        key,
        new Map(Object.entries(value as object)),
      ]),
    );
  } catch (e) {
    const message = e instanceof Error ? e.message : e;
    throw new Error(`Failed to parse mapping: ${String(message)}`);
  }
};

export const defaultMappings: Mappings = new Map([
  [
    'payment',
    new Map([
      ['date', 'date'],
      ['payee', 'payeeName'],
      ['notes', 'notes'],
    ]),
  ],
  [
    'deposit',
    new Map([
      ['date', 'date'],
      ['payee', 'payeeName'],
      ['notes', 'notes'],

View on GitHub (pinned to d4334cb6e6)