Error handling
Exceptions
Section titled “Exceptions”Parser::parseFile()throws aRuntimeExceptionwhen the file does not exist or cannot be readParser::parse()andParser::parseFile()throw aSilarhi\Qif\Exception\ParseExceptionon a value they cannot read without guessing: a date that does not exist or is not a date, an amount that is not a number or is ambiguous (1,234), a split without amountWriter::saveToFile()throws aRuntimeExceptionwhen the directory is not writable or the file cannot be openedTransaction::setAmount()andaddSplit()throw anInvalidArgumentExceptionwhen given a string that is not an amount or is ambiguous ('1,234')
The messages include the path or the value, as written in the file, and the parse errors start with where they happened:
<?php
use Silarhi\Qif\Enums\Types;use Silarhi\Qif\Exception\ParseException;use Silarhi\Qif\Parser;use Silarhi\Qif\Transaction;use Silarhi\Qif\Writer;
try { Parser::parseFile('missing.qif');} catch (RuntimeException $e) { echo $e->getMessage(), PHP_EOL;}
try { (new Writer())->saveToFile('/missing/directory/export.qif');} catch (RuntimeException $e) { echo $e->getMessage(), PHP_EOL;}
$records = [ "!Type:Bank\nD31/02/2026\n^", // a date that does not exist "!Type:Bank\nT1,234\n^", // 1234 or 1.234? "!Type:Bank\nT12 EUR\n^", // not a number "!Type:Bank\nT-20\nSTax\n^", // a split without its $ line];foreach ($records as $content) { try { (new Parser($content))->parse(); } catch (ParseException $e) { echo $e->getMessage(), PHP_EOL; }}
// Where it happened, and the transactions read before$parser = new Parser("!Type:Bank\nD25/12/2026\nT-20\n^\n\nD26/12/2026\nT12 EUR\n^\n");try { $parser->parse();} catch (ParseException $e) { printf("Line %d, record index %d: %s\n", $e->getLineNumber(), $e->getRecordIndex(), $e->getReason()); printf("%d transaction read before it\n", count($parser->getTransactions()));}
try { (new Transaction(Types::Bank))->setAmount('1,234');} catch (InvalidArgumentException $e) { echo $e->getMessage(), PHP_EOL;}File not found or not readable: missing.qifDirectory not writable: /missing/directoryLine 2 (record index 0): "31/02/2026" is not a valid date with the day first (DateFormat::DayFirst).Line 2 (record index 0): "1,234" is ambiguous: "," can be a decimal or a thousands separator. Pass the decimal separator explicitly.Line 2 (record index 0): "12 EUR" is not an amount.Line 3 (record index 0): The split "Tax" has no amount ($ line).Line 7, record index 1: "12 EUR" is not an amount.1 transaction read before it"1,234" is ambiguous: "," can be a decimal or a thousands separator. Pass the decimal separator explicitly.ParseException extends RuntimeException: catching RuntimeException around parseFile() catches both the file
errors and the parse errors. The full list, with every message, is in the exceptions reference.
Where it happened
Section titled “Where it happened”A ParseException stops the parsing at the first value it cannot read. It tells where that value is:
| Method | Returns |
|---|---|
getLineNumber() |
The line of the value, from 1, empty lines included. A split without amount is reported on its S line |
getRecordIndex() |
The index the transaction would have had in getTransactions(), from 0: the number read before it |
getReason() |
The message without the location: "12 EUR" is not an amount. |
The transactions read before the error are not lost: after a failed parse(), getTransactions() returns them.
Parser::parseFile() does not return the parser when it throws: to keep them, read the file yourself and parse it
with the constructor:
use Silarhi\Qif\Exception\ParseException;use Silarhi\Qif\Parser;
$parser = new Parser(file_get_contents('/path/to/export.qif'));try { $parser->parse();} catch (ParseException $e) { error_log($e->getMessage());}
$transactions = $parser->getTransactions(); // every transaction before the errorFixing a ParseException
Section titled “Fixing a ParseException”The messages below are the reasons, without the Line 2 (record index 0): prefix.
| Message | Cause | Fix |
|---|---|---|
"01/31/2026" is not a valid date with the day first… |
Month-first dates (US versions of Quicken) | Pass DateFormat::MonthFirst: see Dates |
"31/02/2026" is not a valid date… |
A date that does not exist with either format | Fix the file |
"1,234" is ambiguous… |
A single , between 1 to 3 digits and 3 digits: 1234 or 1.234 |
Pass the DecimalSeparator of the file: see Amounts |
"…" is not an amount with "." as decimal separator., "…" does not group its thousands by ",". |
An amount in the other convention than the DecimalSeparator passed |
Pass the other DecimalSeparator, or none to detect it |
"12 EUR" is not an amount. |
Not a number | Fix the file |
The split "Tax" has no amount ($ line). |
An S line without its $ line, or with an empty one |
Fix the file |
What does not throw
Section titled “What does not throw”The parser never stores a guessed value, but it accepts records with missing fields and lines it does not know. Check what matters to you after parsing:
| Input | Result |
|---|---|
A record without D or T line |
getDate() or getAmount() returns null |
An empty field (a bare P, D or T line) |
Skipped: the getter returns null, as for a missing field |
A record without any value (a stray ^, only empty fields) |
Skipped: it is not a transaction |
An unknown !Type: (!Type:Memorized) |
getType() returns null |
!Account, !Type:Cat, !Type:Class, !Type:Security, !Type:Prices lists |
Skipped: they hold no transactions |
!Option: and !Clear: lines |
Ignored |
| An unknown cleared status | Status::NotCleared |
A last record without its closing ^ |
Kept |
A $ or E line that does not follow an S line |
Ignored (the transfer amount of investment records) |
| A line with an unknown or ignored code | Ignored: see How a file is read |
| Content that is not QIF at all | Transactions with null fields, or a ParseException |
For instance, to reject the transactions without a date:
foreach ($parser->getTransactions() as $transaction) { if (null === $transaction->getDate()) { throw new UnexpectedValueException('Transaction without a date: ' . $transaction->_raw); }}