I've tried many formal techniques, but what I always come back to is: Break the project down into a list of modules that you will actually have to write or modify. Look at the number of different screens, the number of reports, and any particularly complex logic that will have to be developed. Then guess how long it will take to write each of these individual pieces and add them up.
Yes, at the low level you're still just making up a number, but if you have any experience you should be able to come up with a fairly good estimate for "write a screen to input customer name and address type information and write it to the database". That's something manageable. Trying to estimate "write a payroll system" with no breakdown is much more difficult.
This is essentially the idea of function point analysis. If using the formulas of function point analysis helps you, go ahead. I find it easier to just wing it when I get to the lowest level.
Don't estimate based on the assumption that the first draft of the code will always work perfectly the first time. You know that never happens except for the most trivial programs, but people always seem to estimate based on that assumption. Build in time for finding and fixing bugs. Take it for granted that there will be some number of particular nasty bugs that take a long time to figure out.
Include time for testing. And -- here's where I used to fall down all the time -- include time for fixing the bugs that are found during testing, and then for another round of testing after the fixes. I used to do estimates where I said "2 weeks for testing" or whatever, estimating how much time it would take the test group to get through the system, and then left it at that. That was dumb. Of course the test group will find problems, and you will have to fix them.
(Side note: I used to have a chief tester who would set a goal for each new release of our product that he would find 100 bugs. He saw this as a personal challenge. Sometimes he had to stretch the definition of a bug to make it, like counting a mis-spelled label on a screen as a "bug", but he almost always made it. That's the best kind of tester you can have. I've often had to explain to testers that the goal of testing is not to prove that there are no bugs, but to find the bugs that are surely there.)
Depending on just what portion of the project you're supposed to be estimating and how your organization works, you may also need to plan for "clarifications" to the requirements and more changes when the users see the program in operation. Yes yes, we always say that the requirements must be nailed down before we start coding and once the user signs off no changes will be allowed. I have never worked for an organization where it actually happened this way. No matter what policy is written on a piece of paper, in real life the users always find that even if you implemented exactly what they asked for, when they see it in practice it doesn't really work out, and so there will be further changes and rework.