I've also run into this problem. I've checked the API documentation both for craft\elements\Entry and craft\base\Element and neither seem to have a simple, straightforward method to check if a field exists. Here are a couple of options I have found:
Getting the list of fields
You can get the list of fields by going through the field layout:
// this works with any element
$elementFields = $element->getFieldLayout()->getFields();
// this works only with entries
$entryFields = $entry->getType()->getFieldLayout()->getFields();
// transform the array of Field objects into an array of field handles for convenience
$entryFieldHandles = array_column($entryFields, 'handle');
// check if the entry has my_custom_field
$entryHasMyCustomField = in_array('my_custom_field', $entryFieldHandles);
This also works well in Twig:
{% set entry_field_handles = entry.getFieldLayout().getFields()|column('handle') %}
{% set entry_has_field = 'my_custom_field' in entry_field_handles %}
Downside: May be slow, is a bit verbose, not sure how it interacts with different entry types in a section.
Going through the CustomFieldBehavior
Craft 3 compiles all your fields to the class CustomFieldBehavior, which is then attached to the Element object as a behavior. This class has properties for each custom field and a method canGetProperty which you can use to check if a particular property exists:
$entryHasMyCustomField = $entry->getBehavior('customFields')
->canGetProperty('my_custom_field');
Downsides: Uses undocumented methods / behavior which might change. There might also be some edge-cases if the CustomFieldBehavior class has different properties that aren't fields but match any of your field names, though that's pretty unlikely.
Catching the error
Simple but effective:
try {
$fieldValue = $entry->getFieldValue('my_custom_field');
} catch (\craft\errors\InvalidFieldException $error) {
// field doesn't exist
}
Downside: Really ugly and verbose.